package cn.itcast.nems.journal.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.itcast.ic.common.exception.v2.BizExceptionProducer;
import cn.itcast.nems.approval.bpm.eventstrategy.BpmEventStrategy;
import cn.itcast.nems.approval.dao.entity.ApprovalConfig;
import cn.itcast.nems.approval.dao.entity.ApprovalForm;
import cn.itcast.nems.approval.enumeration.ApprovalErrorBodyEnum;
import cn.itcast.nems.approval.enumeration.ApprovalFormStateEnum;
import cn.itcast.nems.approval.service.ApprovalAdditionalFormService;
import cn.itcast.nems.approval.service.ApprovalConfigService;
import cn.itcast.nems.approval.service.ApprovalFormFlowService;
import cn.itcast.nems.approval.service.ApprovalFormService;
import cn.itcast.nems.approval.service.ApprovalFormService.ApproveResult;
import cn.itcast.nems.approval.service.ApprovalFormService.JournalType;
import cn.itcast.nems.base.api.SubjectApi;
import cn.itcast.nems.base.dao.dataobject.AdditionalFormPaymentchannel;
import cn.itcast.nems.base.dao.entity.E2Clazz;
import cn.itcast.nems.base.dao.mapper.E2ClazzMapper;
import cn.itcast.nems.common.PageQueryModel;
import cn.itcast.nems.common.account.AccountHolder;
import cn.itcast.nems.common.account.AccountInfo;
import cn.itcast.nems.common.constant.PaymentTypeEum;
import cn.itcast.nems.common.enumeration.BizNumberTypeEnum;
import cn.itcast.nems.common.enumeration.NemsErrorBodyEnum;
import cn.itcast.nems.common.service.BizNumberService;
import cn.itcast.nems.common.tenant.TenantInfo;
import cn.itcast.nems.common.tenant.TenantInfoHolder;
import cn.itcast.nems.common.util.NumberUtil;
import cn.itcast.nems.discounts.dao.dataobject.EmsDiscountsDO;
import cn.itcast.nems.discounts.dao.mapper.EmsDiscountsMapper;
import cn.itcast.nems.employee.dao.bean.EmployeeGroupsBean;
import cn.itcast.nems.employee.dao.entity.Employee;
import cn.itcast.nems.employee.service.EmployeeService;
import cn.itcast.nems.journal.api.JournalTransferApi;
import cn.itcast.nems.journal.dto.*;
import cn.itcast.nems.journal.enumeration.JournalStateEnum;
import cn.itcast.nems.journal.enumeration.JournalTransferLineStateEnum;
import cn.itcast.nems.journal.service.JournalTransferCommonService;
import cn.itcast.nems.journal.service.JournalTransferReasonServiceV2;
import cn.itcast.nems.journal.service.JournalTransferService;
import cn.itcast.nems.manager.activity.BpmWorkflowManager;
import cn.itcast.nems.manager.msas.MsasManager;
import cn.itcast.nems.manager.util.CustomBeanUtil;
import cn.itcast.nems.manager.weixin.WeixinManager;
import cn.itcast.nems.oldems.api.StudentApi;
import cn.itcast.nems.oldems.dao.mapper.StudentJoinClassMapper;
import cn.itcast.nems.oldems.dto.*;
import cn.itcast.nems.oldems.dto.oldapi.EmsApiResult;
import cn.itcast.nems.oldems.service.DiscountsService;
import cn.itcast.nems.oldems.service.sync.OldEmsSyncService;
import cn.itcast.nems.order.api.JournalDiscountApi;
import cn.itcast.nems.order.dto.JournalDiscountApiReqDTO;
import cn.itcast.nems.order.dto.JournalDiscountApiResDTO;
import cn.itcast.nems.order.dto.PaymentFlowDTO;
import cn.itcast.nems.product.scheduler.TuitionFeeService;
import cn.itcast.nems.studentrecord.bo.JournalClazzModel;
import cn.itcast.nems.studentrecord.constant.DiscountScopeTypeEnum;
import cn.itcast.nems.studentrecord.constant.DiscountTypeEnum;
import cn.itcast.nems.studentrecord.constant.ReferenceTypeEnum;
import cn.itcast.nems.studentrecord.converter.StudentRecordJournalConverter;
import cn.itcast.nems.studentrecord.dao.condition.ApprovalType;
import cn.itcast.nems.studentrecord.dao.condition.JournalTransferParams;
import cn.itcast.nems.studentrecord.dao.condition.SearchClazzParams;
import cn.itcast.nems.studentrecord.dao.dataobject.JournalRenewalDO;
import cn.itcast.nems.studentrecord.dao.dataobject.JournalTransferLineDiscountDO;
import cn.itcast.nems.studentrecord.dao.dataobject.JournalTransferPageDataExt;
import cn.itcast.nems.studentrecord.dao.entity.*;
import cn.itcast.nems.studentrecord.dao.mapper.*;
import cn.itcast.nems.studentrecord.dto.TransferOrderStateUpdateParams;
import cn.itcast.nems.studentrecord.dto.change.StudentRecordChangeDiscountDTO;
import cn.itcast.nems.studentrecord.dto.change.StudentRecordChangeResultDTO;
import cn.itcast.nems.studentrecord.dto.change.StudentRecordJournalDTO;
import cn.itcast.nems.studentrecord.dto.change.StudentRecordTransferDTO;
import cn.itcast.nems.studentrecord.enumeration.StudentRecordErrorBodyEnum;
import cn.itcast.nems.studentrecord.enumeration.TeachingModelEnum;
import cn.itcast.nems.studentrecord.service.settlement.SettlementResult;
import cn.itcast.nems.studentrecord.service.studentrecord.StudentRecordFundFlowService;
import cn.itcast.nems.studentrecord.service.studentrecord.StudentRecordService;
import cn.itcast.nems.studentrecord.service.studentrecord.StudentRecordTransferService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static cn.itcast.nems.common.util.StreamUtils.distinctByKey;
import static cn.itcast.nems.journal.util.TransferUtils.*;
import static cn.itcast.nems.journal.util.TransferUtils.formatAccount;

@Service
@RequiredArgsConstructor
@Slf4j
public class JournalTransferServiceImpl extends ServiceImpl<JournalTransferMapper, JournalTransfer> implements JournalTransferService, JournalTransferApi, BpmEventStrategy {
    private final JournalTransferMapper journalTransferMapper;
    private final JournalTransferDiscountMapper journalTransferDiscountMapper;
    private final JournalTransferLineMapper journalTransferLineMapper;
    private final JournalTransferLineDiscountMapper journalTransferLineDiscountMapper;
    private final MsasManager msasManager;
    private final SubjectApi subjectApi;
    private final E2ClazzMapper e2ClazzMapper;
    private final BizNumberService bizNumberService;
    private final BpmWorkflowManager bpmWorkflowManager;
    private final StudentApi studentApi;
    private final StudentRecordService studentRecordService;
    private final StudentRecordStudentRelationshipMapper studentRecordStudentRelationshipMapper;
    private final TuitionFeeService tuitionFeeService;
    private final EmsDiscountsMapper emsDiscountsMapper;
    private final DiscountsService discountsService;
    private final StudentRecordTransferService studentRecordTransferService;

    private final ApprovalFormService approvalFormService;
    private final ApprovalFormFlowService approvalFormFlowService;
    private final ApprovalConfigService approvalConfigService;

    private final JournalTransferOrderRelationshipMapper journalTransferOrderRelationshipMapper;
    private final JournalTransferReasonServiceV2 reasonService;
    private final JournalDiscountApi journalDiscountApi;

    private final JournalTransferCommonService commonService;

    private final StudentRecordFundFlowService fundFlowService;
    private final ApprovalAdditionalFormService approvalAdditionalFormService;

    private final OldEmsSyncService oldEmsSyncService;

    private final EmployeeService employeeService;

    private final WeixinManager weixinManager;
    private final StudentJoinClassMapper studentJoinClassMapper;

    @Value("${journal.check-form.transfer}")
    private Boolean checkForm;

    /**
     * 转班凭单页面，搜索班级
     *
     * @param queryModel
     * @return
     */
    @Override
    public cn.itcast.ic.common.bean.Page<Map> searchClazzes(PageQueryModel<JournalClazzModel> queryModel) {
        ApprovalConfig config = approvalConfigService.getByJournalCode(JournalType.TRANSFER.name());
        cn.hutool.core.lang.Assert.notNull(config, BizExceptionProducer.produce(ApprovalErrorBodyEnum.MUST_CONFIG_APPROVAL_CONFIG, JournalType.TRANSFER.getCnName()));
        var formIdList = bpmWorkflowManager.findWaitApproval(config.getBpmProcessKey(), AccountHolder.getRequiredAccount());
        Map<String, String> journalIdFormIdMap = journalIdFormIdMap(queryModel.getCondition().getTypes(), formIdList);
        Page<JournalRenewalDO> page = new Page<>();
        page.setCurrent(queryModel.getPageNumber());
        page.setSize(queryModel.getPageSize());
        JournalClazzModel clazzModel = Objects.nonNull(queryModel.getCondition()) ? queryModel.getCondition() : new JournalClazzModel();
        var params = CustomBeanUtil.copyProperties(clazzModel, new SearchClazzParams());
        if (clazzModel.getApprovalType() == ApprovalType.MY_COMMIT) {
            params.setScopeOrCreatedBy(AccountHolder.getRequiredAccount());
        } else if (clazzModel.getApprovalType() == ApprovalType.WAIT_APPROVAL) {
            params.setScopeOrIdIn(journalIdFormIdMap.keySet().stream().toList());
        } else if (clazzModel.getApprovalType() == ApprovalType.APPROVED) {
            params.setScopeOrIdInCreatedBy(AccountHolder.getRequiredAccount());
        } else if (clazzModel.getApprovalType() == null || clazzModel.getApprovalType() == ApprovalType.ALL) {
            params.setNotDraft(true);
        }
        var condition = queryModel.getCondition();
        String journalType;
        if (condition.getTypes() == null || condition.getTypes().isEmpty()) {
            journalType = JournalType.TRANSFER.name()
                    + "," + JournalType.EXCHANGE.name();
        } else {
            journalType = condition.getTypes().stream().map(JournalTransferType::name).collect(Collectors.joining(","));
        }
        params.setJournalType(journalType);
        if (condition.getApprovalType() == ApprovalType.MY_COMMIT) {
            params.setFlowCreatedBy(AccountHolder.getRequiredAccount());
            params.setCreatedBy(AccountHolder.getRequiredAccount());
        } else if (condition.getApprovalType() == ApprovalType.WAIT_APPROVAL) {
            // 待办
            params.setFormIds(formIdList);
            params.setFlowCreatedBy(AccountHolder.getRequiredAccount());
            params.setNotDraft(true);
        } else if (condition.getApprovalType() == ApprovalType.APPROVED) {
            // 已办
            params.setFormIds(formIdList);
            params.setFlowCreatedBy(AccountHolder.getRequiredAccount());
            params.setNotDraft(true);
        } else if (condition.getApprovalType() == ApprovalType.ALL) {
            // 全部
            params.setNotDraft(true);
            EmployeeGroupsBean employeeGroups = AccountHolder.getRequiredAccountEmployeeGroups();
            if (employeeGroups.getGroupList().isEmpty()) {
                return cn.itcast.ic.common.bean.Page.of((int) queryModel.getPageNumber(), (int) queryModel.getPageSize(), 0, null);
            }
            params.setEmployeeGroups(employeeGroups.getGroupList());
        }
        // 查询内容并返回DTO
        IPage<Map> pageData = journalTransferMapper.searchClazzes(page, params);
        return cn.itcast.ic.common.bean.Page.of(queryModel.getPageNumber(), queryModel.getPageSize(), Long.valueOf(pageData.getTotal()).intValue(), pageData.getRecords());
    }

    /**
     * 转班凭单页面，info.getId()分页
     *
     * @param condition
     * @param objectPage
     * @return
     */
    @Override
    public cn.itcast.ic.common.bean.Page<JournalTransferInListVDto> page(JournalTransferQuery condition, Page<?> objectPage) {
        // 取所有等待自己审批的凭单ID，并且建立 JournalId -> FormId 的映射MAP
        var config = getApprovalConfig(condition.getTypes());
        var formIdList = bpmWorkflowManager.findWaitApproval(config.getBpmProcessKey(), AccountHolder.getRequiredAccount());
        Map<String, String> journalIdFormIdMap = journalIdFormIdMap(condition.getTypes(), formIdList);
        var params = CustomBeanUtil.copyProperties(condition, new JournalTransferParams());
        if (condition.getApprovalType() == ApprovalType.MY_COMMIT) {
            params.setScopeOrCreatedBy(AccountHolder.getRequiredAccount());
        } else if (condition.getApprovalType() == ApprovalType.WAIT_APPROVAL) {
            params.setScopeOrIdIn(journalIdFormIdMap.keySet().stream().toList());
        } else if (condition.getApprovalType() == ApprovalType.APPROVED) {
            params.setScopeOrIdInCreatedBy(AccountHolder.getRequiredAccount());
        } else if (condition.getApprovalType() == null || condition.getApprovalType() == ApprovalType.ALL) {
            params.setNotDraft(true);
        }
        String journalType;
        if (condition.getTypes() == null || condition.getTypes().isEmpty()) {
            journalType = JournalType.TRANSFER.name()
                    + "," + JournalType.EXCHANGE.name();
        } else {
            journalType = condition.getTypes().stream().map(JournalTransferType::name).collect(Collectors.joining(","));
        }
        params.setJournalType(journalType);
        if (condition.getApprovalType() == ApprovalType.MY_COMMIT) {
            params.setFlowCreatedBy(AccountHolder.getRequiredAccount());
            params.setCreatedBy(AccountHolder.getRequiredAccount());
        } else if (condition.getApprovalType() == ApprovalType.WAIT_APPROVAL) {
            // 待办
            params.setFormIds(formIdList);
            params.setFlowCreatedBy(AccountHolder.getRequiredAccount());
            params.setNotDraft(true);
        } else if (condition.getApprovalType() == ApprovalType.APPROVED) {
            // 已办
            params.setFormIds(formIdList);
            params.setFlowCreatedBy(AccountHolder.getRequiredAccount());
            params.setNotDraft(true);
        } else if (condition.getApprovalType() == ApprovalType.ALL) {
            // 全部
            params.setNotDraft(true);
            EmployeeGroupsBean employeeGroups = AccountHolder.getRequiredAccountEmployeeGroups();
            if (employeeGroups.getGroupList().isEmpty()) {
                return cn.itcast.ic.common.bean.Page.of((int) objectPage.getCurrent(), (int) objectPage.getSize(), 0, null);
            }
            params.setEmployeeGroups(employeeGroups.getGroupList());
        }
        // 查询内容并返回DTO
        IPage<JournalTransferPageDataExt> dbPage = journalTransferMapper.selectPageData(objectPage, params);
        final cn.itcast.ic.common.bean.Page<JournalTransferInListVDto> pageData = cn.itcast.ic.common.bean.Page.of(
                Long.valueOf(dbPage.getCurrent()).intValue(),
                Long.valueOf(dbPage.getSize()).intValue(),
                Long.valueOf(dbPage.getTotal()).intValue(),
                mapToVDto(dbPage.getRecords(), journalIdFormIdMap)
        );
        return pageData;
    }

    /**
     * 获取journalId与formId的对应关系
     *
     * @param types      凭单类型
     * @param formIdList 表单ID列表
     * @return
     */
    @NotNull
    private Map<String, String> journalIdFormIdMap(List<JournalTransferType> types, List<String> formIdList) {
        Map<String, String> journalIdFormIdMap = new HashMap<>();
        var wp = new QueryWrapper<ApprovalForm>()
                .lambda()
                .in(ApprovalForm::getId, formIdList);
        if (types != null && !types.isEmpty()) {
            wp.in(ApprovalForm::getJournalType, types);
        }
        if (!formIdList.isEmpty()) {
            journalIdFormIdMap = approvalFormService.page(
                            Page.of(1, 1000),
                            wp
                    ).getRecords().stream()
                    .filter(distinctByKey(ApprovalForm::getJournalId))
                    .collect(Collectors.toMap(ApprovalForm::getJournalId, ApprovalForm::getId));
        }
        return journalIdFormIdMap;
    }

    /**
     * 转化成DTO
     */
    private List<JournalTransferInListVDto> mapToVDto(List<JournalTransferPageDataExt> records, Map<String, String> journalIdFormIdMap) {
        // 查找所有非带我审批的凭单，并且建立 JournalId -> FormId 的映射MAP
        // 以创建时间排倒序，并distinctByKey(ApprovalForm::getJournalId)，以取最新的审批单
        var searchPending = records.stream()
                .filter(e -> !journalIdFormIdMap.containsKey(e.getId()))
                .map(JournalTransfer::getId)
                .collect(Collectors.toList());
        Map<String, String> pendingMap;
        if (!searchPending.isEmpty()) {
            pendingMap = approvalFormService.page(
                            Page.of(1, 1000),
                            new QueryWrapper<ApprovalForm>()
                                    .lambda()
                                    .in(ApprovalForm::getJournalId, searchPending)
                                    .orderByDesc(ApprovalForm::getCreatedDatetime)
                    ).getRecords().stream().filter(distinctByKey(ApprovalForm::getJournalId))
                    .collect(Collectors.toMap(ApprovalForm::getJournalId, ApprovalForm::getId));
        } else {
            pendingMap = new HashMap<>();
        }
        // 转化成DTO
        var result = records.stream().map(e -> getJournalTransferInListVDto(journalIdFormIdMap, e, pendingMap))
                .collect(Collectors.toList());
        if (!result.isEmpty()) {
            var createdByEmailList = result.stream().map(JournalTransferInListVDto::getCreatedBy).collect(Collectors.toSet());
            var map = employeeService.findMoreByEmail(createdByEmailList).stream().collect(Collectors.toMap(
                    Employee::getEmail,
                    e -> e
            ));
            for (JournalTransferInListVDto journalTransferInListVDto : result) {
                var email = journalTransferInListVDto.getCreatedBy();
                var employee = map.get(email);
                if (employee != null) {
                    journalTransferInListVDto.setCreatedByName(accountName(employee, email));
                    journalTransferInListVDto.setCreatedByAndName(formatAccount(employee, email));
                }
            }
        }
        return result;
    }

    /**
     * 转化成dto
     *
     * @param journalIdFormIdMap
     * @param e
     * @param pendingMap
     * @return
     */
    @NotNull
    private JournalTransferInListVDto getJournalTransferInListVDto(
            Map<String, String> journalIdFormIdMap, JournalTransferPageDataExt e,
            Map<String, String> pendingMap
    ) {
        return new JournalTransferInListVDto(
                e.getId(),
                e.getState(),
                e.getTargetClazzId(),
                e.getTargetClazzCampusName(),
                e.getTargetClazzName(),
                e.getTargetClazzPrice(),
                formatPrice(e.getTargetClazzPrice()),
                e.getTransferQuantity(),
                e.getRemark(),
                e.getCreatedBy(),
                null,
                null,
                e.getCreatedDatetime(),
                journalIdFormIdMap.containsKey(e.getId()),
                journalIdFormIdMap.getOrDefault(e.getId(), pendingMap.get(e.getId())),
                e.getExtend1(),
                e.getExtend2(),
                e.getExtend3(),
                e.getSpecialFlag(),
                e.getType(),
                e.getCategory()
        );
    }

    @Override
    @Transactional
    public String create(JournalTransferCreateBo bo) {
        var account = AccountHolder.getRequiredAccount();
        var data = new JournalTransfer();
        data.setBizDate(LocalDateTime.now());
        data.setTargetClazzId(bo.getTargetClazzId());
        var e2Clazz = journalClazz(bo);
        data.setTargetClazzName(e2Clazz.getClazzName());
        var targetClazzOriginalAmount = clazzOriginAmount(e2Clazz);
        data.setTargetClazzPrice(targetClazzOriginalAmount);
        data.setTargetClazzSubjectId(bo.getTargetClazzSubjectId());
        data.setTargetClazzSubjectName(subjectApi.findById(bo.getTargetClazzSubjectId()).getName());
        data.setTargetClazzCampusId(bo.getTargetClazzCampusId());
        Assert.isTrue(e2Clazz.getSchoolCode().equals(bo.getTargetClazzCampusId()), "校区不匹配");
        data.setTargetClazzCampusName(e2Clazz.getSchoolName());
        data.setTargetClazzModelId(bo.getTargetClazzModelId());
        data.setTargetClazzModelName(TeachingModelEnum.valueOf(bo.getTargetClazzModelId()).getName());
        data.setTransferQuantity(0);
        if (StringUtils.hasText(bo.getState())) {
            Assert.isTrue(
                    List.of("DRAFT", "PENDING").contains(bo.getState()),
                    "创建凭单时，且只能存为草稿(DRAFT)或者提交(PENDING)"
            );
            if ("PENDING".equals(bo.getState())) {
                Assert.notNull(bo.getSingleLine(), "创建凭单并且提交时，必须有表单行");
            }
            data.setState(bo.getState());
        } else {
            data.setState("DRAFT");
        }
        data.setType(bo.getType()); // 使用枚举，前后端类型对应上
        data.setSpecialFlag(bo.getSpecialFlag());
        data.setRemark(bo.getRemark());
        data.setTenantId(TenantInfo.DEFAULT_TENANT_ID);
        data.setSubtenantId(TenantInfo.DEFAULT_SUBTENANT_ID);
        data.setCreatedBy(account);
        data.setCreatedDatetime(LocalDateTime.now());
        data.setDeletedFlag(Boolean.FALSE);
        // “转班”、“转移” 使用原ID生成逻辑，“休学”、“转待定”使用新的ID生成逻辑
        data.setId(journalId(data.getType()));
        // 处理休学原因等冗余字段
        doReasonExtendField(bo);
        data.setExtend1(bo.getExtend1());
        data.setExtend2(bo.getExtend2());
        data.setExtend3(bo.getExtend3());
        // 如果是休学等单行，直接保存成有一个表单行的凭单
        if (bo.getSingleLine() != null) {
            data.setTransferQuantity(1);
        }
        journalTransferMapper.insert(data);
        // 处理单行凭单
        bo.setId(data.getId());
        bo.setState(data.getState());
        processSingleLine(bo);
        // 返回结果
        return data.getId();
    }

    @Override
    @Transactional
    public String updateSingleLine(JournalTransferCreateBo bo) {
        // 更新主体
        var targetClazz = journalClazz(bo);
        var data = new JournalTransfer();
        doReasonExtendField(bo);
        data.setExtend1(bo.getExtend1());
        data.setExtend2(bo.getExtend2());
        data.setExtend3(bo.getExtend3());
        journalTransferMapper.update(
                null,
                new UpdateWrapper<JournalTransfer>()
                        .lambda()
                        .eq(JournalTransfer::getId, bo.getId())
                        .set(JournalTransfer::getModifiedBy, AccountHolder.getRequiredAccount())
                        .set(JournalTransfer::getModifiedDatetime, LocalDateTime.now())
                        .set(JournalTransfer::getTargetClazzId, targetClazz.getClazzId())
                        .set(JournalTransfer::getTargetClazzName, targetClazz.getClazzName())
                        .set(JournalTransfer::getTargetClazzPrice, 0)
                        .set(JournalTransfer::getTargetClazzSubjectId, targetClazz.getSubjectId())
                        .set(JournalTransfer::getTargetClazzSubjectName, targetClazz.getClazzName())
                        .set(JournalTransfer::getTargetClazzCampusId, targetClazz.getSchoolCode())
                        .set(JournalTransfer::getTargetClazzCampusName, targetClazz.getSchoolName())
                        .set(JournalTransfer::getTargetClazzModelId, targetClazz.getTeachingModel())
                        .set(JournalTransfer::getTargetClazzModelName, TeachingModelEnum.valueOf(targetClazz.getTeachingModel()).getName())
                        .set(JournalTransfer::getTransferQuantity, 1)
                        .set(JournalTransfer::getState, bo.getState())
                        .set(JournalTransfer::getType, bo.getType())
                        .set(JournalTransfer::getSpecialFlag, bo.getSpecialFlag())
                        .set(JournalTransfer::getRemark, bo.getRemark())
                        .set(JournalTransfer::getExtend1, data.getExtend1())
                        .set(JournalTransfer::getExtend2, data.getExtend2())
                        .set(JournalTransfer::getExtend3, data.getExtend3())
        );
        // 更新从表
        var line = bo.getSingleLine();
        line.setJournalTransferId(data.getId());
        line.setState(bo.getState());
        updateSingleLinePaidInfo(line);
        journalTransferLineMapper.update(
                null,
                new UpdateWrapper<JournalTransferLine>()
                        .lambda()
                        .eq(JournalTransferLine::getJournalTransferId, bo.getId())
                        .set(JournalTransferLine::getModifiedBy, AccountHolder.getRequiredAccount())
                        .set(JournalTransferLine::getModifiedDatetime, LocalDateTime.now())
                        .set(JournalTransferLine::getOriginStudentRecordId, line.getOriginStudentRecordId())
                        .set(JournalTransferLine::getOriginClazzId, line.getOriginClazzId())
                        .set(JournalTransferLine::getOriginClazzName, line.getOriginClazzName())
                        .set(JournalTransferLine::getOriginClazzInDate, line.getOriginClazzInDate())
                        .set(JournalTransferLine::getOriginClazzOutDate, line.getOriginClazzOutDate())
                        .set(JournalTransferLine::getOriginClazzOriginalAmount, line.getOriginClazzOriginalAmount())
                        .set(JournalTransferLine::getOriginClazzDiscountAmount, line.getOriginClazzDiscountAmount())
                        .set(JournalTransferLine::getOriginClazzAmountDue, line.getOriginClazzAmountDue())
                        .set(JournalTransferLine::getOriginActualAmountDue, line.getOriginActualAmountDue())
                        .set(JournalTransferLine::getOriginClazzSettlementAmount, line.getOriginClazzSettlementAmount())
                        .set(JournalTransferLine::getOriginClazzActualSettlementAmount, line.getOriginClazzActualSettlementAmount())
                        .set(JournalTransferLine::getOriginClazzServiceDays, line.getOriginClazzServiceDays())
                        .set(JournalTransferLine::getOriginClazzSettlementDays, line.getOriginClazzSettlementDays())
                        .set(JournalTransferLine::getOriginClazzActualSettlementDays, line.getOriginClazzActualSettlementDays())
                        .set(JournalTransferLine::getTargetClazzInDate, line.getTargetClazzInDate())
                        .set(JournalTransferLine::getTargetClazzOriginalAmount, line.getTargetClazzOriginalAmount())
                        .set(JournalTransferLine::getTargetClazzDiscountAmount, line.getTargetClazzDiscountAmount())
                        .set(JournalTransferLine::getTargetClazzAmountDue, line.getTargetClazzAmountDue())
                        .set(JournalTransferLine::getTargetClazzActualAmountDue, line.getTargetClazzActualAmountDue())
                        .set(JournalTransferLine::getTargetClazzTransferAmount, line.getTargetClazzTransferAmount())
                        .set(JournalTransferLine::getPriceDifferenceFlag, line.getPriceDifferenceFlag())
                        .set(JournalTransferLine::getPriceDifferenceAmount, line.getPriceDifferenceAmount())
                        .set(JournalTransferLine::getReasonCode, line.getReasonCode())
                        .set(JournalTransferLine::getReasonDescription, line.getReasonDescription())
                        .set(JournalTransferLine::getMultiPaymentAccountFlag, line.getMultiPaymentAccountFlag())
                        .set(JournalTransferLine::getOriginPaymentAccountCode, line.getOriginPaymentAccountCode())
                        .set(JournalTransferLine::getOriginPaymentAccountName, line.getOriginPaymentAccountName())
        );
        var record = studentRecordService.getById(line.getOriginStudentRecordId());
        Assert.notNull(record, "学员学籍数据未找到");
        Assert.isTrue("VALID".equals(record.getState()), "学员学籍状态不是有效状态");
        Assert.isTrue(
                !bo.getTargetClazzId().equals(record.getClazzId()),
                "转入班不能和转出班相同"
        );
        Assert.isTrue(!record.getLockFlag(), "学员已经被锁定");
        Assert.isTrue(record.getSettlementDate() == null, "学员已经出班, 不能提交");
        var studentId = studentRecordStudentRelationshipMapper.selectStudentId(record.getId());
        Assert.notNull(studentId, "学员未找到");
        var count = studentRecordStudentRelationshipMapper.countValid(studentId, bo.getTargetClazzId());
        Assert.isTrue(count == 0, "学员已经在目标班级, 且学籍有效");
        if ("PENDING".equals(line.getState())) {
            lockLineRecords(Set.of(line.getOriginStudentRecordId()), bo.getType(), bo.getId());
            this.createWorkflow(bo.getType(), bo.getId(), bo.getSpecialFlag(), data.getExtend3(), List.of(line));
        }
        return bo.getId();
    }

    @Override
    public JournalTransferInfoVDto info(String id) {
        var journal = journalTransfer(id);
        var lines = journalLines(journal.getId());
        var discounts = journalDiscounts(journal.getId());
        var lineDiscounts = journalLineDiscounts(journal.getId());
        var lineDiscountGroupByLineId = groupLineDiscounts(lineDiscounts);
        var lineIdOriginStudentReocrdMap = lineIdOriginStudentReocrdMap(lines);
        var lineIdOriginStudentIdMap = lineIdOriginStudentIdMap(lineIdOriginStudentReocrdMap);
        var lineIdStudentMap = lineIdStudentMap(lineIdOriginStudentIdMap);
        var lineIdOriginClazzMap = lineIdOriginClazzMap(lineIdOriginStudentReocrdMap);
        var mergeDiscountMap = mergeDiscountMap(discounts, lineDiscounts);

        // 回显
        JournalTransferInfoVDto vDto = CustomBeanUtil.copyProperties(journal, new JournalTransferInfoVDto());
        vDto.setTargetClazzPriceFormatted(formatPrice(journal.getTargetClazzPrice()));
        // todo discount金额需要取值
        vDto.setDiscountList(discounts.stream().map(e -> mapToVo(e, mergeDiscountMap)).collect(Collectors.toList()));
        vDto.setLineList(lines
                .stream()
                .map(e -> {
                    final var vo = CustomBeanUtil.copyProperties(e, new JournalTransferLineVDto());
                    formatPrices(vo);
                    List<JournalTransferLineDiscountVDto> list = lineDiscountGroupByLineId.getOrDefault(e.getId(), new ArrayList<>());
                    vo.setDiscountList(list.stream()
                            .filter(i -> DiscountScopeTypeEnum.NORMAL.name().equalsIgnoreCase(i.getDiscountScopeType())).sorted().toList());
                    vo.setDiscountCouponList(list.stream()
                            .filter(i -> DiscountScopeTypeEnum.PERSONAL.name().equalsIgnoreCase(i.getDiscountScopeType())).sorted().toList());
                    var clazz = lineIdOriginClazzMap.get(e.getId());
                    if (clazz != null) {
                        vo.setOriginClazzGraduationDate(clazz.getGraduationDate());
                        vo.setOriginClazzPredictionGraduationDate(clazz.getPredictionGraduationDate());
                    }
                    return vo;
                })
                .collect(Collectors.toList()));
        if (!vDto.getLineList().isEmpty()) {
            var lineIdList = vDto.getLineList().stream().map(JournalTransferLineVDto::getId).toList();
            var orderMap = journalTransferOrderRelationshipMapper.selectList(new QueryWrapper<JournalTransferOrderRelationship>()
                            .lambda()
                            .in(JournalTransferOrderRelationship::getJournalTransferLineId, lineIdList)
                            .eq(JournalTransferOrderRelationship::getDeletedFlag, Boolean.FALSE)
                    ).stream()
                    .collect(Collectors.toMap(
                            JournalTransferOrderRelationship::getJournalTransferLineId,
                            JournalTransferOrderRelationship::getOrderId
                    ));
            vDto.getLineList()
                    .forEach(
                            line -> line.setOrderId(orderMap.get(line.getId()))
                    );
            var lineIdTargetStudentReocrdMap = lineIdTargetStudentReocrdMap(vDto.getLineList());
            vDto.getLineList()
                    .forEach(
                            line -> {
                                var record = lineIdTargetStudentReocrdMap.get(line.getId());
                                if (record != null) {
                                    line.setStudentRecordState(record.getState());
                                }
                            }
                    );
        }
        var account = employeeService.findOneByEmail(vDto.getCreatedBy());
        vDto.setCreatedByName(accountName(account, vDto.getCreatedBy()));
        vDto.setCreatedByAndName(formatAccount(account, vDto.getCreatedBy()));
        //
        for (JournalTransferLineVDto e : vDto.getLineList()) {
            var student = lineIdStudentMap.get(e.getId());
            if (student != null) {
                e.setStudentId(student.getId());
                e.setStudentName(student.getName());
                e.setStudentNumber(student.getNumber());
            }
        }
        //
        vDto.getDiscountList().forEach(e -> {
            var discount = mergeDiscountMap.get(e.getDiscountId());
            if (discount != null) {
                e.setDiscountRate(discount.getDiscountsRate());
            }
        });
        //
        final var clazz = journalClazz(vDto);
        vDto.setTargetClazzStartDate(clazz.getStartDate());
        vDto.setTargetClazzTypeId(clazz.getClazzTypeId());
        vDto.setTargetClazzUpgradeable(clazz.getUpgradeable());
        vDto.setTargetClazzPredictionGraduationDate(clazz.getPredictionGraduationDate());
        vDto.setFormId(loadFormId(vDto.getId()));
        vDto.setReasonCode(journal.getExtend1());
        vDto.setReasonDescription(journal.getExtend2());
        vDto.setDirection(journal.getExtend3());
        return vDto;
    }

    @NotNull
    private static JournalTransferDiscountVDto mapToVo(JournalTransferDiscount e, Map<Integer, EmsDiscountsDO> map) {
        final var vo = CustomBeanUtil.copyProperties(e, new JournalTransferDiscountVDto());
        var dO = map.get(e.getDiscountId());
        if (dO != null) {
            vo.setDiscountAmount(dO.getDiscountsMoney().multiply(new BigDecimal(100)).intValue());
            vo.setDiscountAmountFormatted(formatPrice(vo.getDiscountAmount()));
            vo.setDiscountRate(dO.getDiscountsRate());
        }
        return vo;
    }

    /**
     * 格式化金钱用于前端展示
     *
     * @param vo
     */
    private static void formatPrices(JournalTransferLineVDto vo) {
        vo.setOriginClazzOriginalAmountFormatted(formatPrice(vo.getOriginClazzOriginalAmount()));
        vo.setOriginClazzDiscountAmountFormatted(formatPrice(vo.getOriginClazzDiscountAmount()));
        vo.setOriginClazzAmountDueFormatted(formatPrice(vo.getOriginClazzAmountDue()));
        vo.setOriginActualAmountDueFormatted(formatPrice(vo.getOriginActualAmountDue()));
        vo.setOriginClazzSettlementAmountFormatted(formatPrice(vo.getOriginClazzSettlementAmount()));
        vo.setOriginClazzActualSettlementAmountFormatted(formatPrice(vo.getOriginClazzActualSettlementAmount()));
        vo.setTargetClazzOriginalAmountFormatted(formatPrice(vo.getTargetClazzOriginalAmount()));
        vo.setTargetClazzDiscountAmountFormatted(formatPrice(vo.getTargetClazzDiscountAmount()));
        vo.setTargetClazzAmountDueFormatted(formatPrice(vo.getTargetClazzAmountDue()));
        vo.setTargetClazzActualAmountDueFormatted(formatPrice(vo.getTargetClazzActualAmountDue()));
        vo.setTargetClazzTransferAmountFormatted(formatPrice(vo.getTargetClazzTransferAmount()));
        vo.setPriceDifferenceAmountFormatted(formatPrice(vo.getPriceDifferenceAmount()));
    }


    @Override
    public String loadFormId(String journalId) {
        var forms = approvalFormService.page(
                Page.of(1, 1),
                new QueryWrapper<ApprovalForm>()
                        .lambda()
                        .eq(ApprovalForm::getJournalId, journalId)
                        .orderByDesc(ApprovalForm::getCreatedDatetime)
        ).getRecords();
        if (!forms.isEmpty()) {
            return (forms.get(0).getId());
        }
        return null;
    }

    @Override
    @Transactional
    public Boolean update(JournalTransferInfoBDto info) {
        // 检查前端数据结构的合法性
        this.updateCheckWebInput(info);
        // 检查操作是否正确
        Assert.isTrue(List.of("DRAFT", "PENDING", "COMMIT").contains(info.getState()), "只能存为草稿或者提交");
        // 检查数据是否存在，并且状态合理
        var journal = journalTransferMapper.selectById(info.getId());
        Assert.notNull(journal, "数据不存在");
        Assert.isTrue("DRAFT".equals(journal.getState()), "只能修改草稿状态的数据");
        // 如果是保存操作或者是提交操作，需要从新入库
        if (List.of("DRAFT", "PENDING").contains(info.getState())) {
            this.flushDataWhenUpdate(info, journal);
            if ("PENDING".equals(info.getState())) {
                journal = journalTransferMapper.selectById(info.getId());
            }
        }
        // COMMIT = 不更新数据直接 PENDING
        if ("COMMIT".equals(info.getState())) {
            updateJournalToState(info.getId(), ApprovalFormStateEnum.PENDING);
        }
        // 如果是COMMIT或者PENDING，需要锁定学员，并创建审批流
        if (List.of("COMMIT", "PENDING").contains(info.getState())) {
            // lock student
            var lines = journalLinesRequired(info.getId());
            var recordIdList = lines.stream().map(JournalTransferLine::getOriginStudentRecordId).collect(Collectors.toList());
            Assert.isTrue(!recordIdList.isEmpty(), "学生不能为空");
            var records = studentRecordService.getByIds(recordIdList);
            Assert.isTrue(records.size() == lines.size(), "学员学籍数据未找到");
            if ("COMMIT".equals(info.getState())) {
                JournalTransfer finalJournal = journal;
                records.forEach(r -> Assert.isTrue(
                        !finalJournal.getTargetClazzId().equals(r.getClazzId()),
                        "转入班不能和转出班相同"
                ));
            } else if ("PENDING".equals(info.getState())) {
                records.forEach(r -> Assert.isTrue(
                        !info.getTargetClazzId().equals(r.getClazzId()),
                        "转入班不能和转出班相同"
                ));
            }
            for (StudentRecord record : records) {
                Assert.isTrue(!record.getLockFlag(), "学员已经被锁定");
                Assert.isTrue(record.getSettlementDate() == null, "学员已经出班, 不能提交");
                Assert.isTrue("VALID".equals(record.getState()), "学员学籍状态不是有效状态");
            }
            journalTransferLineMapper.update(
                    null,
                    new UpdateWrapper<JournalTransferLine>().lambda()
                            .in(JournalTransferLine::getId, lines.stream().map(JournalTransferLine::getId).collect(Collectors.toList()))
                            .set(JournalTransferLine::getState, JournalTransferLineStateEnum.PENDING)
            );
            if ("COMMIT".equals(info.getState())) {
                checkTargetClazz(journal.getTargetClazzId(), lines);
            } else if ("PENDING".equals(info.getState())) {
                checkTargetClazz(info.getTargetClazzId(), lines);
            }
            studentRecordService.locks(
                    lines.stream().map(JournalTransferLine::getOriginStudentRecordId).collect(Collectors.toSet()),
                    typeTranslateReference(journal.getType()),
                    journal.getId()
            );
            if ("PENDING".equals(info.getState())) {
                journal = journalTransferMapper.selectById(info.getId());
            }
            final Integer classId = journal.getTargetClazzId();
            // 查询校验个人优惠券是否可用
            List<JournalTransferLineDiscountDO> discounList = journalTransferLineDiscountMapper.findListByTransferId(info.getId(), DiscountScopeTypeEnum.PERSONAL.name());
            List<JournalDiscountApiResDTO> resList = Objects.requireNonNullElse(discounList, new ArrayList<JournalTransferLineDiscountDO>()).stream()
                    .map(item -> this.getPersonDiscount(classId, item.getOriginStudentRecordId(), item.getJournalDiscountLineId(), true))
                    .toList();
            ;
            log.info("【转班凭单】获取个人优惠单列表：{}", JSON.toJSONString(resList));
            this.createWorkflow(journal.getType(), journal.getId(), journal.getSpecialFlag(), journal.getExtend3(), lines);
        }
        return Boolean.TRUE;
    }

    private void checkTargetClazz(Integer targetClazzId, List<JournalTransferLine> lines) {
        var lineIdOriginStudentReocrdMap = lineIdOriginStudentReocrdMap(lines);
        var lineIdOriginStudentIdMap = lineIdOriginStudentIdMap(lineIdOriginStudentReocrdMap);
        var findValidNames = studentRecordStudentRelationshipMapper.findValidNames(
                lineIdOriginStudentIdMap.values().stream().toList(),
                targetClazzId
        );
        if (!findValidNames.isEmpty()) {
            Assert.isTrue(
                    false,
                    "学员已经在目标班级, 且学籍有效：" + String.join(",", findValidNames)
            );
        }
    }

    private JournalDiscountApiResDTO getPersonDiscount(Integer targetClazzId, String originStudentRecordId, String journalDiscountLineId, Boolean validate) {
        JournalDiscountApiResDTO apiResDTO = journalDiscountApi.queryByLineId(JournalDiscountApiReqDTO.buildForQueryByLineId(journalDiscountLineId));
        cn.hutool.core.lang.Assert.notNull(apiResDTO, BizExceptionProducer.produce(NemsErrorBodyEnum.DATA_EXIST, "个人优惠行ID：" + journalDiscountLineId));
        if (validate) {
            StudentRecordStudentRelationship studentRecordStudentRelationship = studentRecordStudentRelationshipMapper.selectOne(
                    new QueryWrapper<StudentRecordStudentRelationship>().lambda()
                            .in(StudentRecordStudentRelationship::getStudentRecordId, originStudentRecordId)
                            .eq(StudentRecordStudentRelationship::getDeletedFlag, false)
                            .orderByDesc(StudentRecordStudentRelationship::getCreatedDatetime).last(" limit 1"));
            cn.hutool.core.lang.Assert.notNull(studentRecordStudentRelationship, BizExceptionProducer.produce(NemsErrorBodyEnum.DATA_EXIST, "学籍ID：" + originStudentRecordId + " 对应学籍学生关联信息"));
            JournalDiscountApiReqDTO reqDTO = JournalDiscountApiReqDTO.buildForWriteOff(journalDiscountLineId, studentRecordStudentRelationship.getStudentId(), null, targetClazzId, LocalDateTime.now(), JournalDiscountApiReqDTO.Scenario.RENEWAL);
            log.info("【转班凭单】调用校验个人优惠券是否有效参数：{}", JSON.toJSON(reqDTO));
            cn.hutool.core.lang.Assert.isTrue(journalDiscountApi.validityVerify(reqDTO), BizExceptionProducer.produce(StudentRecordErrorBodyEnum.PERSONAL_DISCOUNT_USED, journalDiscountLineId));
        }
        return apiResDTO;
    }

    /**
     * 更新操作时重组（清洗）数据库内的表单行数据
     *
     * @param info
     * @param journal
     */
    private void flushDataWhenUpdate(
            JournalTransferInfoBDto info,
            JournalTransfer journal
    ) {
        // 重新赋值：班级、授课模式、科目、校区
        var targetClazzOriginalAmount = setTargetClazzInfoAndReturnOriginAmount(info);
        // 设置目标班级执行价格
        // 重新赋值：个数，并检查有没有重复的学员
        checkRecords(info);
        info.setTransferQuantity(info.getLineList().size());
        //
        if (StringUtils.hasText(info.getReasonCode())) {
            var reason = reasonService.all().getByTypeAndCode(
                    info.getType(),
                    info.getReasonCode()
            );
            if (reason != null) {
                info.setReasonDescription(reason.getName());
            } else {
                info.setReasonCode(null);
                info.setReasonDescription(null);
            }
        } else {
            info.setReasonCode(null);
            info.setReasonDescription(null);
        }
        // 如果是PENDING，重新计算优惠，防止前端数据被篡改 (已修改，代替前端赋值)
        {
            var mainDiscountIdList = info.getDiscountList().stream().map(JournalTransferInfoBDtoDiscount::getDiscountId).toList();
            Map<Integer, EmsDiscountsDO> mainDMap = emsDiscoutMap(mainDiscountIdList);
            var i = 0;
            for (JournalTransferInfoBDtoDiscount journalTransferInfoBDtoDiscount : info.getDiscountList()) {
                EmsDiscountsDO discountsDO = mainDMap.get(journalTransferInfoBDtoDiscount.getDiscountId());
                Assert.notNull(discountsDO, String.format("转班凭单 优惠项id :%s不存在", journalTransferInfoBDtoDiscount.getDiscountId()));
                journalTransferInfoBDtoDiscount.setDiscountName(discountsDO.getDiscountsName());
                journalTransferInfoBDtoDiscount.setDiscountType(Objects.nonNull(discountsDO.getDiscountsStatus()) && discountsDO.getDiscountsStatus() == 1 ? DiscountTypeEnum.DISCOUNT.name() : DiscountTypeEnum.CASH.name());
                journalTransferInfoBDtoDiscount.setSort(String.valueOf(i++));
                journalTransferInfoBDtoDiscount.setDescription("");
            }
            var recordIdList = info.getLineList().stream().map(JournalTransferInfoBDtoLine::getOriginStudentRecordId).collect(Collectors.toList());
            if ("PENDING".equals(info.getState())) {
                Assert.isTrue(!recordIdList.isEmpty(), "学生不能为空");
            }
            if (!recordIdList.isEmpty()) {
                var records = studentRecordService.getByIds(recordIdList);
                Assert.isTrue(records.size() == info.getLineList().size(), "学员学籍数据未找到");
                records.stream().forEach(r -> Assert.isTrue(
                        !info.getTargetClazzId().equals(r.getClazzId()),
                        "转入班不能和转出班相同"
                ));
                var recordChannelMap = recordChannelMap(recordIdList);
                if ("PENDING".equals(info.getState())) {
                    for (StudentRecord record : records) {
                        Assert.isTrue(!record.getLockFlag(), "学员已经被锁定");
                        Assert.isTrue(record.getSettlementDate() == null, "学员已经出班, 不能提交");
                        Assert.isTrue("VALID".equals(record.getState()), "学员学籍状态不是有效状态");
                    }
                }
                var recordMap = records.stream().collect(Collectors.toMap(StudentRecord::getId, e -> e));
                var clazzIdList = records.stream().map(StudentRecord::getClazzId).distinct().collect(Collectors.toList());
                var clazzList = e2ClazzMapper.selectList(new QueryWrapper<E2Clazz>()
                        .lambda()
                        .in(E2Clazz::getClazzId, clazzIdList));
                Assert.isTrue(clazzList.size() == clazzIdList.size(), "班级数据未找到");
                var discountIds = info.getLineList().stream()
                        .flatMap(e -> Objects.requireNonNullElse(e.getDiscountList(), new ArrayList<JournalTransferInfoBDtoLineDiscount>()).stream())
                        .peek(e -> Assert.notNull(e.getDiscountId(), "优惠ID不能为空")
                        ).map(JournalTransferInfoBDtoLineDiscount::getDiscountId).collect(Collectors.toSet());
                Map<Integer, EmsDiscountsDO> discountMap = null;
                if (!discountIds.isEmpty()) {
                    log.info("【转班凭单】普通优惠项：{}", JSON.toJSONString(discountIds));
                    var allDiscounts = emsDiscountsMapper.findByIdList(discountIds.stream().toList());
                    Assert.isTrue(allDiscounts.size() == discountIds.size(), "优惠数据未找到");
                    discountMap = allDiscounts.stream().collect(Collectors.toMap(EmsDiscountsDO::getId, e -> e));
                } else {
                    discountMap = Map.of();
                }
                var clazzMap = clazzList.stream().collect(Collectors.toMap(E2Clazz::getClazzId, e -> e));
                for (JournalTransferInfoBDtoLine journalTransferInfoBDtoLine : info.getLineList()) {
                    var studentRecord = recordMap.get(journalTransferInfoBDtoLine.getOriginStudentRecordId());
                    var sourceClazz = clazzMap.get(studentRecord.getClazzId());
                    journalTransferInfoBDtoLine.setReasonCode(info.getReasonCode());
                    journalTransferInfoBDtoLine.setReasonDescription(info.getReasonDescription());
                    journalTransferInfoBDtoLine.setOriginClazzId(sourceClazz.getClazzId());
                    // 需求改动 【20240301】
                    // https://zentao.itcast.cn/task-view-2101.html?tid=wdqt9mqi
                    // 【新EMS-转班凭单】学籍卡信息与【新EMS-转班-详情】班级信息不一致。
                    // 【解决方案】取学籍的班级名称
                    // journalTransferInfoBDtoLine.setOriginClazzName(sourceClazz.getClazzName());
                    journalTransferInfoBDtoLine.setOriginClazzName(studentRecord.getClazzName());
                    journalTransferInfoBDtoLine.setOriginClazzInDate(studentRecord.getInDate().toLocalDate());
                    final var settlementResult = commonService.calculate(
                            info.getType(),
                            studentRecord,
                            journalTransferInfoBDtoLine.getOriginClazzOutDate().atTime(0, 0),
                            sourceClazz
                    );
                    var shouldPayStart = switch (info.getType()) {
                        case EXCHANGE, SUSPENSION, DELAY ->
                                studentRecord.getActualAmountDue() - settlementResult.getSettlementAmount();
                        default -> targetClazzOriginalAmount;
                    };
                    Integer targetClazzDiscountAmount = 0;
                    // 计算主要优惠
                    if (info.getDiscountList() != null && !info.getDiscountList().isEmpty()) {
                        for (JournalTransferInfoBDtoDiscount journalTransferInfoBDtoDiscount : info.getDiscountList()) {
                            var shouldPayStartBD = new BigDecimal(shouldPayStart).divide(new BigDecimal(100));
                            var discount = mainDMap.get(journalTransferInfoBDtoDiscount.getDiscountId());
                            var dis = new BigDecimal(discountsService.transferDiscountDTO(discount, shouldPayStartBD).getShowDiscountMoney()).multiply(new BigDecimal(100)).intValue();
                            targetClazzDiscountAmount += dis;
                        }
                    }
                    var shouldPayStartBD = new BigDecimal(shouldPayStart).divide(new BigDecimal(100));

                    if (CollectionUtil.isNotEmpty(journalTransferInfoBDtoLine.getDiscountList())) {
                        for (JournalTransferInfoBDtoLineDiscount journalTransferInfoBDtoLineDiscount : journalTransferInfoBDtoLine.getDiscountList()) {
                            var discount = discountMap.get(journalTransferInfoBDtoLineDiscount.getDiscountId());
                            Assert.notNull(discount, "优惠数据未找到");
                            journalTransferInfoBDtoLineDiscount.setDiscountName(discount.getDiscountsName());
                            journalTransferInfoBDtoLineDiscount.setDiscountType(Objects.nonNull(discount.getDiscountsStatus()) && discount.getDiscountsStatus() == 1 ? DiscountTypeEnum.DISCOUNT.name() : DiscountTypeEnum.CASH.name());
                            var dis = new BigDecimal(discountsService.transferDiscountDTO(discount, shouldPayStartBD).getShowDiscountMoney()).multiply(new BigDecimal(100)).intValue();
                            journalTransferInfoBDtoLineDiscount.setDiscountAmountFormatted(formatPrice(dis));
                            targetClazzDiscountAmount += dis;
                        }
                    }
                    // 校验个人优惠单 、计算优惠合计
                    log.info("【转班凭单】页面个人优惠单列表：{}", JSON.toJSONString(journalTransferInfoBDtoLine.getDiscountCouponList()));
                    if (CollectionUtil.isNotEmpty(journalTransferInfoBDtoLine.getDiscountCouponList())) {
                        for (JournalTransferInfoBDtoLineDiscount journalTransferInfoBDtoLineDiscount : journalTransferInfoBDtoLine.getDiscountCouponList()) {
                            var discount = journalDiscountApi.queryByLineId(JournalDiscountApiReqDTO.buildForQueryByLineId(journalTransferInfoBDtoLineDiscount.getJournalDiscountLineId()));
                            Assert.notNull(discount, "个人优惠数据未找到");
                            journalTransferInfoBDtoLineDiscount.setDiscountName(discount.getDiscountName());
                            journalTransferInfoBDtoLineDiscount.setDiscountType(discount.getDiscountType());
                            var dis = NumberUtil.convertToCent(discount.getDiscountValue());
                            // 如果优惠类型为 “折扣” ， 优惠金额 =  执行价格 *  折扣
                            if (DiscountTypeEnum.DISCOUNT.name().equalsIgnoreCase(discount.getDiscountType())) {
                                dis = NumberUtil.convertToCent(shouldPayStartBD.multiply(discount.getDiscountValue()));
                            }
                            log.info("【个人优惠项】 学籍ID：{} 金额：{}", journalTransferInfoBDtoLine.getOriginStudentRecordId(), dis);
                            journalTransferInfoBDtoLineDiscount.setDiscountAmountFormatted(formatPrice(dis));
                            targetClazzDiscountAmount += dis;
                        }
                    }
                    if (targetClazzDiscountAmount > shouldPayStart) {
                        targetClazzDiscountAmount = shouldPayStart;
                    }
                    Integer finalShouldPay = shouldPayStart - targetClazzDiscountAmount;
                    Boolean diff = (studentRecord.getActualAmountDue() - settlementResult.getSettlementAmount()) != finalShouldPay;
                    Integer diffPrice = (studentRecord.getActualAmountDue() - settlementResult.getSettlementAmount()) - finalShouldPay;
                    journalTransferInfoBDtoLine.setOriginClazzOriginalAmountFormatted(formatPrice(studentRecord.getPrice()));
                    journalTransferInfoBDtoLine.setOriginClazzDiscountAmountFormatted(formatPrice(studentRecord.getDiscountAmount()));
                    journalTransferInfoBDtoLine.setOriginClazzAmountDueFormatted(formatPrice(studentRecord.getAmountDue()));
                    journalTransferInfoBDtoLine.setOriginActualAmountDueFormatted(formatPrice(settlementResult.getSettlementAmount()));
                    journalTransferInfoBDtoLine.setOriginClazzSettlementAmountFormatted(formatPrice(settlementResult.getSettlementAmount()));
                    journalTransferInfoBDtoLine.setOriginClazzActualSettlementAmountFormatted(journalTransferInfoBDtoLine.getOriginClazzSettlementAmountFormatted());
                    journalTransferInfoBDtoLine.setOriginClazzServiceDays(settlementResult.getServiceDays());
                    journalTransferInfoBDtoLine.setOriginClazzSettlementDays(settlementResult.getSettlementDays());
                    journalTransferInfoBDtoLine.setOriginClazzActualSettlementDays(journalTransferInfoBDtoLine.getOriginClazzSettlementDays());
                    journalTransferInfoBDtoLine.setTargetClazzOriginalAmountFormatted(formatPrice(targetClazzOriginalAmount));
                    journalTransferInfoBDtoLine.setTargetClazzDiscountAmountFormatted(formatPrice(targetClazzDiscountAmount));
                    journalTransferInfoBDtoLine.setTargetClazzAmountDueFormatted(formatPrice(finalShouldPay));
                    journalTransferInfoBDtoLine.setTargetClazzActualAmountDueFormatted(formatPrice(finalShouldPay));
                    journalTransferInfoBDtoLine.setTargetClazzTransferAmountFormatted(formatPrice(settlementResult.getTransferAmount()));
                    journalTransferInfoBDtoLine.setPriceDifferenceFlag(diff);
                    journalTransferInfoBDtoLine.setPriceDifferenceAmountFormatted(formatPrice(diffPrice));
                    var sFundFlows = recordChannelMap.get(studentRecord.getId());
                    journalTransferInfoBDtoLine.setMultiPaymentAccountFlag(sFundFlows.multiPaymentAccountFlag);
                    journalTransferInfoBDtoLine.setOriginPaymentAccountCode(sFundFlows.paymentAccountCode);
                    journalTransferInfoBDtoLine.setOriginPaymentAccountName(sFundFlows.paymentAccountName);
                }
            }
        }
        // todo check info ： 班级校区是否对应等？
        updateJournalTableInfo(info);
        // todo check discounts ，
        // discountAmount字段等应该去掉, 固没有赋值
        removeOldJournalDiscounts(info.getId());
        for (JournalTransferInfoBDtoDiscount journalTransferInfoBDtoDiscount : info.getDiscountList()) {
            var discount = mapToDiscountEntity(info, journalTransferInfoBDtoDiscount);
            journalTransferDiscountMapper.insert(discount);
        }
        // lines
        removeOldLines(info.getId());
        removeOldLineDiscounts(info.getId());
        for (JournalTransferInfoBDtoLine line : info.getLineList()) {
            var journalTransferLine = mapToLineEntity(info, line);
            journalTransferLineMapper.insert(journalTransferLine);
            // 校验页面优惠参数、计算行优惠合计（凭单头优惠+行优惠项）, 进行一些冗余
            line.setId(journalTransferLine.getId());
            line.setJournalTransferId(info.getId());
            line.setDiscountList(getLineDiscountList(line));
            saveLineDiscounts(line);
        }
    }

    @NotNull
    private static JournalTransferDiscount mapToDiscountEntity(JournalTransferInfoBDto info, JournalTransferInfoBDtoDiscount journalTransferInfoBDtoDiscount) {
        var discount = new JournalTransferDiscount();
        discount.setJournalTransferId(info.getId());
        discount.setDiscountId(journalTransferInfoBDtoDiscount.getDiscountId());
        discount.setDiscountName(journalTransferInfoBDtoDiscount.getDiscountName());
        discount.setDiscountType(journalTransferInfoBDtoDiscount.getDiscountType());
        discount.setDiscountAmount(0);
        discount.setSort(journalTransferInfoBDtoDiscount.getSort());
        discount.setDescription(journalTransferInfoBDtoDiscount.getDescription());
        discount.setTenantId(TenantInfo.DEFAULT_TENANT_ID);
        discount.setSubtenantId(TenantInfo.DEFAULT_SUBTENANT_ID);
        discount.setCreatedBy(AccountHolder.getRequiredAccount());
        discount.setCreatedDatetime(LocalDateTime.now());
        discount.setDeletedFlag(Boolean.FALSE);
        return discount;
    }

    @NotNull
    private static JournalTransferLine mapToLineEntity(JournalTransferInfoBDto info, JournalTransferInfoBDtoLine line) {
        var journalTransferLine = new JournalTransferLine();
        journalTransferLine.setJournalTransferId(info.getId());
        journalTransferLine.setOriginStudentRecordId(line.getOriginStudentRecordId());
        journalTransferLine.setOriginClazzId(line.getOriginClazzId());
        journalTransferLine.setOriginClazzName(line.getOriginClazzName());
        journalTransferLine.setOriginClazzInDate(line.getOriginClazzInDate());
        journalTransferLine.setOriginClazzOutDate(line.getOriginClazzOutDate());
        journalTransferLine.setOriginClazzOriginalAmount(parsePrice(line.getOriginClazzOriginalAmountFormatted()));
        journalTransferLine.setOriginClazzDiscountAmount(parsePrice(line.getOriginClazzDiscountAmountFormatted()));
        journalTransferLine.setOriginClazzAmountDue(parsePrice(line.getOriginClazzAmountDueFormatted()));
        journalTransferLine.setOriginActualAmountDue(parsePrice(line.getOriginActualAmountDueFormatted()));
        journalTransferLine.setOriginClazzSettlementAmount(parsePrice(line.getOriginClazzSettlementAmountFormatted()));
        journalTransferLine.setOriginClazzActualSettlementAmount(parsePrice(line.getOriginClazzActualSettlementAmountFormatted()));
        journalTransferLine.setOriginClazzServiceDays(line.getOriginClazzServiceDays());
        journalTransferLine.setOriginClazzSettlementDays(line.getOriginClazzSettlementDays());
        journalTransferLine.setOriginClazzActualSettlementDays(line.getOriginClazzActualSettlementDays());
        journalTransferLine.setTargetClazzInDate(line.getTargetClazzInDate());
        journalTransferLine.setTargetClazzOriginalAmount(parsePrice(line.getTargetClazzOriginalAmountFormatted()));
        journalTransferLine.setTargetClazzDiscountAmount(parsePrice(line.getTargetClazzDiscountAmountFormatted()));
        journalTransferLine.setTargetClazzAmountDue(parsePrice(line.getTargetClazzAmountDueFormatted()));
        journalTransferLine.setTargetClazzActualAmountDue(parsePrice(line.getTargetClazzActualAmountDueFormatted()));
        journalTransferLine.setTargetClazzTransferAmount(parsePrice(line.getTargetClazzTransferAmountFormatted()));
        journalTransferLine.setPriceDifferenceFlag(line.getPriceDifferenceFlag());
        journalTransferLine.setPriceDifferenceAmount(parsePrice(line.getPriceDifferenceAmountFormatted()));
        journalTransferLine.setState(line.getState());
        journalTransferLine.setStudentRecordId(line.getStudentRecordId());
        journalTransferLine.setTenantId(TenantInfo.DEFAULT_TENANT_ID);
        journalTransferLine.setSubtenantId(TenantInfo.DEFAULT_SUBTENANT_ID);
        journalTransferLine.setCreatedBy(AccountHolder.getRequiredAccount());
        journalTransferLine.setCreatedDatetime(LocalDateTime.now());
        journalTransferLine.setDeletedFlag(Boolean.FALSE);
        journalTransferLine.setReasonCode(line.getReasonCode());
        journalTransferLine.setReasonDescription(line.getReasonDescription());
        journalTransferLine.setMultiPaymentAccountFlag(line.getMultiPaymentAccountFlag());
        journalTransferLine.setOriginPaymentAccountCode(line.getOriginPaymentAccountCode());
        journalTransferLine.setOriginPaymentAccountName(line.getOriginPaymentAccountName());
        return journalTransferLine;
    }

    /**
     * 删除凭单
     *
     * @param id
     * @return
     */
    @Override
    public Boolean delete(String id) {
        var journal = journalTransferMapper.selectById(id);
        Assert.notNull(journal, "数据不存在");
        Assert.isTrue("DRAFT".equals(journal.getState()), "只能删除草稿状态的数据");
        Assert.isTrue(journal.getCreatedBy().equals(AccountHolder.getRequiredAccount()), "只能删除自己创建的数据");
        journalTransferMapper.update(null, new UpdateWrapper<JournalTransfer>().lambda()
                .eq(JournalTransfer::getId, id)
                .set(JournalTransfer::getDeletedBy, AccountHolder.getRequiredAccount())
                .set(JournalTransfer::getDeletedDatetime, LocalDateTime.now())
                .set(JournalTransfer::getDeletedFlag, Boolean.TRUE));
        return Boolean.TRUE;
    }

    @Override
    @Transactional
    public void cancel(String formID) {
        ApprovalForm approvalForm = approvalFormService.getById(formID);
        String journalId = approvalForm.getJournalId();
        var j = journalTransferMapper.selectById(journalId);
        cn.hutool.core.lang.Assert.notNull(j, BizExceptionProducer.produce(NemsErrorBodyEnum.DATA_EXIST, "ID：" + formID));
        // 校验：必须发起人才能撤回
        Assert.isTrue(j.getCreatedBy().equals(AccountHolder.getRequiredAccount()), "必须发起人才能撤回");
        int updateResult = journalTransferMapper.update(null, new UpdateWrapper<JournalTransfer>().lambda()
                .eq(JournalTransfer::getId, journalId).set(JournalTransfer::getState, JournalStateEnum.DRAFT));
        cn.hutool.core.lang.Assert.isTrue(updateResult > 0,
                BizExceptionProducer.produce(NemsErrorBodyEnum.DATABASE_UPDATE_ERROR, "撤回"));
        unlockLineRecords(j);
        approvalFormService.cancel(formID);
    }

    @Override
    @Transactional
    public void rollbackRequester(String formID) {
        ApprovalForm approvalForm = approvalFormService.getById(formID);
        String journalId = approvalForm.getJournalId();
        var journal = journalTransferMapper.selectById(journalId);
        cn.hutool.core.lang.Assert.notNull(journal, BizExceptionProducer.produce(NemsErrorBodyEnum.DATA_EXIST, "ID：" + formID));
        if (journal.getState().equals(JournalStateEnum.DRAFT.name())) {
            return;
        }
        int updateResult = journalTransferMapper.update(null, new UpdateWrapper<JournalTransfer>().lambda()
                .eq(JournalTransfer::getId, journalId).set(JournalTransfer::getState, JournalStateEnum.DRAFT));
        cn.hutool.core.lang.Assert.isTrue(updateResult > 0,
                BizExceptionProducer.produce(NemsErrorBodyEnum.DATABASE_UPDATE_ERROR, "撤回"));
        unlockLineRecords(journal);
//        approvalFormService.cancel(formID);
    }

    @Override
    public Boolean isHaveWaitApprovingList() {
        // todo 传入类型
        ApprovalConfig config = approvalConfigService.getByJournalCode(JournalType.TRANSFER.name());
        cn.hutool.core.lang.Assert.notNull(config, BizExceptionProducer.produce(ApprovalErrorBodyEnum.MUST_CONFIG_APPROVAL_CONFIG, JournalType.TRANSFER.getCnName()));
        var waitApproval = bpmWorkflowManager.findWaitApproval(config.getBpmProcessKey(), AccountHolder.getRequiredAccount());
        if (!waitApproval.isEmpty()) {
            waitApproval = approvalFormService.page(
                    Page.of(1, 1000),
                    new QueryWrapper<ApprovalForm>()
                            .lambda()
                            .in(ApprovalForm::getId, waitApproval)
            ).getRecords().stream().map(ApprovalForm::getJournalId).collect(Collectors.toList());
        }
        if (waitApproval.isEmpty()) {
            return Boolean.FALSE;
        }
        return journalTransferMapper.selectCount(new QueryWrapper<JournalTransfer>()
                .lambda()
                .in(JournalTransfer::getId, waitApproval)
                .eq(JournalTransfer::getDeletedFlag, Boolean.FALSE)) > 0;
    }

    @Override
    public void onTransferOrderStateUpdate(TransferOrderStateUpdateParams params) {
        journalTransferLineMapper.update(
                null,
                new UpdateWrapper<JournalTransferLine>().lambda()
                        .eq(JournalTransferLine::getJournalTransferId, params.getJournalLineId())
                        .set(JournalTransferLine::getState, params.getOrderState())
        );
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void inform(String formID, String targetAssignee) {
        approvalFormFlowService.inform(formID, targetAssignee);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Runnable finish(String formID) {
        log.info("【转班审批】审批完成，formID：{}", formID);
        ApprovalForm approvalForm = approvalFormService.getById(formID);
        String journalId = approvalForm.getJournalId();
        log.info("【转班审批】审批完成，formID：{} , journalId：{}", formID, journalId);
        // 校验表单是否完整，不完整则返回null
        var ac = checkAddForm(formID, journalId);
        if (ac == null) {
            // 如果nacos配置文件中配置了必须校验附加表单，则将表单置为失败
            if (checkForm) {
                doCheckFail(formID, journalId, ApprovalErrorBodyEnum.MESSAGE_ADDITIONAL_FORM_FIELD_NOT_NULL);
                return null;
            }
            // 否则将继续处理凭单到审批通过状态
            else {
                ac = new AdditionalFormPaymentchannel();
            }
        }
        // 处理凭单自己的业务逻辑
        var doResult = this.doFinish(journalId, ac, approvalForm.getModifiedDatetime());
        // 同步到ems
        syncResultToOldEms(approvalForm, journalId, doResult);
        // 更新凭单状态为“完成”
        updateJournalToFinish(journalId);
        // 处理本地的审批流程单
        approvalFormService.finish(formID);
        // 如果需要增加流水则发送MQ消息
        // new Thread
        if (!doResult.flowDTOs.isEmpty()) {
            studentRecordTransferService.addOrderFlow(doResult.flowDTOs);
        }
        // syncResultToOldEmsAsync(approvalForm, journalId, doResult);
        log.info("【转班审批】审批完成，处理结束 formID：{} , journalId：{}", formID, journalId);
        return () -> syncSjc(journalId);
    }

    private void syncSjc(String journalId) {
        try {
            var lines = journalLines(journalId);
            syncSjc(lines.stream().map(JournalTransferLine::getStudentRecordId).collect(Collectors.toList()));
        } catch (Throwable e) {
            log.error("【转班审批】审批完成，处理结束【】 journalId：" + journalId, e);
        }
    }

    private void syncSjc(List<String> collect) {
        studentRecordService.syncSjc(collect);
    }

    private void syncResultToOldEms(ApprovalForm approvalForm, String journalId, DoFinishResult doResult) {
        if (!doResult.circulations.isEmpty()) {
            final EmsApiResult result = oldEmsSyncService.circulations(doResult.circulations);
            Assert.isTrue(result.isSuccess(), "同步到ems失败:" + result.getErrorMessage());
        }
        if (!doResult.suspendSyncs.isEmpty()) {
            for (SuspendSync suspendSync : doResult.suspendSyncs) {
                final EmsApiResult result = oldEmsSyncService.suspension(
                        suspendSync.studentId,
                        suspendSync.clazzId,
                        suspendSync.suspendDate,
                        suspendSync.operatorEmail
                );
                Assert.isTrue(result.isSuccess(), "同步到ems失败:" + result.getErrorMessage());
            }
        }
        if (!doResult.oldEmsSuspensionReturns.isEmpty()) {
            var journal = journalTransfer(journalId);
            for (Integer sjcId : doResult.oldEmsSuspensionReturns) {
                final EmsApiResult result = oldEmsSyncService.suspensionReturn(sjcId, journal.getCreatedBy());
                Assert.isTrue(result.isSuccess(), "同步到ems失败:" + result.getErrorMessage());
            }
        }
    }

    private final ExecutorService pool = Executors.newFixedThreadPool(1);

    private void syncResultToOldEmsAsync(ApprovalForm approvalForm, String journalId, DoFinishResult doResult) {
        if (!doResult.fundTransfers.isEmpty()) {
            pool.submit(() -> {
                try {
                    TimeUnit.SECONDS.sleep(10);
                    TenantInfoHolder.setTenantInfo(TenantInfo.DEFAULT_TENANT_INFO);
                    AccountHolder.setAccountInfo(new AccountInfo(
                            "admin@itcast.cn",
                            "admin",
                            "0",
                            ""
                    ));
                    for (FundTransfer fundTransfer : doResult.fundTransfers) {
                        try {
                            log.info("【转班】【线程池】【同步流水】 {} {} {}", journalId, approvalForm.getJournalId(), fundTransfer);
                            final EmsApiResult result = oldEmsSyncService.fundTransferByTransfer(
                                    fundTransfer.originStudentRecordId,
                                    fundTransfer.targetStudentRecordId,
                                    fundTransfer.multiPaymentAccountFlag,
                                    fundTransfer.channel,
                                    fundTransfer.type,
                                    fundTransfer.createdBy,
                                    fundTransfer.originChannelCode,
                                    fundTransfer.transferAmount
                            );
                            Assert.isTrue(result.isSuccess(), "同步到ems失败:" + result.getErrorMessage());
                        } catch (Exception e) {
                            log.error("【转班凭单】流水同步到ems失败：{} : {}", fundTransfer, e.getMessage());
                            sendMsg(approvalForm, journalId, fundTransfer, e);
                        }
                    }
                } catch (Exception e) {
                    log.error("【转班凭单】流水同步到ems失败：{}", e.getMessage());
                }
            });
        }
    }

    private void sendMsg(ApprovalForm approvalForm, String journalId, FundTransfer fundTransfer, Exception e) {
        try {
            weixinManager.sendMarkdownMessage(
                    approvalForm.getId() + journalId + fundTransfer.originStudentRecordId + fundTransfer.targetStudentRecordId,
                    String.format(
                            "【转班凭单】流水同步到ems失败：\nFormId：%s\nJournalId：%s\n错误信息：%s\n详情：%s",
                            approvalForm.getId(),
                            journalId,
                            e.getMessage(),
                            fundTransfer.toString()
                    )
            );
        } catch (Exception es) {
            log.error("【转班凭单】发送消息失败", es);
        }
    }

    @Override
    public void finishFail(String formID, Exception exception) {
        ApprovalForm approvalForm = approvalFormService.getById(formID);
        unlockLineRecords(approvalForm.getJournalId());
    }

    @Override
    public String getStrategyName() {
        return JournalType.TRANSFER.name();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approve(String formID, ApproveResult result, String comment, final String form, Integer applyType) {
        ApprovalForm approvalForm = approvalFormService.getByBusinessKey(formID);
        var journalId = approvalForm.getJournalId();
        log.info("【审批转班凭单】凭单头详情：{}", JSON.toJSONString(approvalForm, SerializerFeature.DisableCircularReferenceDetect));
        log.info("【审批转班凭单】：{} , {} , {} , {} , {} , {}", journalId, formID, result, comment, form, applyType);
        if (ApproveResult.REJECT == result) {
            log.info("【审批转班凭单】 驳回 {}", journalId);
            unlockLineRecords(approvalForm.getJournalId());
            updateJournalToState(journalId, JournalStateEnum.REJECTED);
        } else {
            log.info("【审批转班凭单】 审批 {}", journalId);
            JournalStateEnum journalStateEnum = JournalStateEnum.APPROVING;
            //  1：审批（默认））2：驳回到发起人）3：驳回上一级）
            if (applyType != null && applyType == 2) {
                log.info("【审批转班凭单】 驳回到发起人 {}", journalId);
                journalStateEnum = JournalStateEnum.DRAFT;
                unlockLineRecords(journalId);
            } else if (applyType != null && applyType == 3) {
                log.info("【审批转班凭单】 驳回到上一级 {}", journalId);
                journalStateEnum = JournalStateEnum.PENDING;
            }
            lambdaUpdate().eq(JournalTransfer::getId, journalId).set(JournalTransfer::getState, journalStateEnum.name()).update();
        }
        approvalFormService.approve(formID, result, comment, form, applyType);
    }

    @Override
    public void remind(String formID, String targetAssignee) {
        String result = approvalFormService.remind(formID, targetAssignee);
        log.info("【调用工作流催办接口】响应正文：" + result);
    }

    @Override
    public JournalTransferDTO getById(String id) {
        JournalTransfer JournalTransfer = getBaseMapper().selectById(id);
        cn.hutool.core.lang.Assert.notNull(JournalTransfer, BizExceptionProducer.produce(NemsErrorBodyEnum.DATA_EXIST, "转班凭单ID：" + id));
        return CustomBeanUtil.copyProperties(JournalTransfer, new JournalTransferDTO());
    }

    /**
     * 完成凭单的业务逻辑
     *
     * @param id
     * @param ac
     * @param modifiedDatetime
     * @return
     */
    private DoFinishResult doFinish(String id, AdditionalFormPaymentchannel ac, LocalDateTime formModifiedDatetime) {
        var journal = journalTransfer(id);
        var journalDiscounts = journalDiscounts(id);
        var lines = journalLines(id);
        var lineDiscounts = journalLineDiscounts(id);
        var lineIdOriginStudentReocrdMap = lineIdOriginStudentReocrdMap(lines);
        var lineIdOriginStudentIdMap = lineIdOriginStudentIdMap(lineIdOriginStudentReocrdMap);
        var discountMap = discountMap(journalDiscounts, lineDiscounts);
        var lineIdOriginClazzMap = lineIdOriginClazzMap(lineIdOriginStudentReocrdMap);
        var e2Clazz = journalClazz(journal);
        var targetClazzOriginalAmount = clazzOriginAmount(e2Clazz);
        var flowList = new ArrayList<PaymentFlowDTO>();
        List<StudentRecordCirculation> circulations = new ArrayList<>();
        List<SuspendSync> suspendSyncs = new ArrayList<>();
        List<FundTransfer> fundTransfers = new ArrayList<>();
        List<Integer> oldEmsSuspensionReturns = new ArrayList<>();
        for (JournalTransferLine line : lines) {
            var studentRecord = lineIdOriginStudentReocrdMap.get(line.getId());
            var sourceClazz = lineIdOriginClazzMap.get(line.getId());
            var studentId = lineIdOriginStudentIdMap.get(line.getId());
            final var record = setResultBeforeTransfer(line, journal, studentId, studentRecord, sourceClazz, targetClazzOriginalAmount, journalDiscounts, discountMap, lineDiscounts);
            var dto = studentRecordTransferService.transfer(record);
            StudentRecordJournalDTO studentRecordJournal = StudentRecordJournalConverter.transfer(journal, line);
            studentRecordJournal.setTargetStudentRecordId(dto.getTargetStudentRecordId());
            //
            studentRecordJournal.setBizDate(ac.getBizDate());
            studentRecordJournal.setPaymentAccountCode(ac.getChannelCode());
            studentRecordJournal.setPaymentAccountName(ac.getChannelName());
            studentRecordJournal.setMultiPaymentAccountFlag(line.getMultiPaymentAccountFlag());
            studentRecordJournal.setOriginPaymentAccountCode(line.getOriginPaymentAccountCode());
            studentRecordJournal.setOriginPaymentAccountName(line.getOriginPaymentAccountName());
            //
            var sResult = studentRecordTransferService.transferComplete(studentRecordJournal);
            if (sResult.getPaymentFlowDTO() != null) {
                var flow = sResult.getPaymentFlowDTO();
                flowList.add(flow);
            }
            setLineRecordIdAndState(line, dto.getTargetStudentRecordId(), lineFinishState(journal.getType()));
            saveOrderRelationship(line, journal, dto);
            oldEmsSyncInfo(ac, line, journal, suspendSyncs, record, oldEmsSuspensionReturns, studentId, studentRecord, studentRecordJournal, fundTransfers, sourceClazz, targetClazzOriginalAmount, e2Clazz, circulations, formModifiedDatetime);
        }
        return new DoFinishResult(
                circulations, flowList, suspendSyncs, fundTransfers,
                oldEmsSuspensionReturns
        );
    }

    @Override // 手动同步
    public void manualSync(ApprovalForm approvalForm) {
        var formID = approvalForm.getId();
        String journalId = approvalForm.getJournalId();
        var doResult = doFinishResultByJournalId(formID, journalId, approvalForm.getModifiedDatetime());
        syncResultToOldEms(approvalForm, journalId, doResult);
    }

    private DoFinishResult doFinishResultByJournalId(String formID, String id, LocalDateTime formModifiedDatetime) {
        var ac = checkAddForm(formID, id);
        var journal = journalTransfer(id);
        var journalDiscounts = journalDiscounts(id);
        var lines = journalLines(id);
        var lineDiscounts = journalLineDiscounts(id);
        var lineIdOriginStudentReocrdMap = lineIdOriginStudentReocrdMap(lines);
        var lineIdOriginStudentIdMap = lineIdOriginStudentIdMap(lineIdOriginStudentReocrdMap);
        var discountMap = discountMap(journalDiscounts, lineDiscounts);
        var lineIdOriginClazzMap = lineIdOriginClazzMap(lineIdOriginStudentReocrdMap);
        var e2Clazz = journalClazz(journal);
        var targetClazzOriginalAmount = clazzOriginAmount(e2Clazz);
        var flowList = new ArrayList<PaymentFlowDTO>();
        List<StudentRecordCirculation> circulations = new ArrayList<>();
        List<SuspendSync> suspendSyncs = new ArrayList<>();
        List<FundTransfer> fundTransfers = new ArrayList<>();
        List<Integer> oldEmsSuspensionReturns = new ArrayList<>();
        for (JournalTransferLine line : lines) {
            var studentRecord = lineIdOriginStudentReocrdMap.get(line.getId());
            var sourceClazz = lineIdOriginClazzMap.get(line.getId());
            var studentId = lineIdOriginStudentIdMap.get(line.getId());
            final var record = setResultBeforeTransfer(line, journal, studentId, studentRecord, sourceClazz, targetClazzOriginalAmount, journalDiscounts, discountMap, lineDiscounts);
            StudentRecordJournalDTO studentRecordJournal = new StudentRecordJournalDTO();
            studentRecordJournal.setTargetStudentRecordId(line.getStudentRecordId());
            oldEmsSyncInfo(
                    ac,
                    line,
                    journal,
                    suspendSyncs,
                    record,
                    oldEmsSuspensionReturns,
                    studentId,
                    studentRecord,
                    studentRecordJournal,
                    fundTransfers,
                    sourceClazz,
                    targetClazzOriginalAmount,
                    e2Clazz,
                    circulations,
                    formModifiedDatetime
            );
        }
        return new DoFinishResult(
                circulations, flowList, suspendSyncs, fundTransfers,
                oldEmsSuspensionReturns
        );
    }

    private void oldEmsSyncInfo(AdditionalFormPaymentchannel ac, JournalTransferLine line, JournalTransfer journal, List<SuspendSync> suspendSyncs, StudentRecordTransferDTO record, List<Integer> oldEmsSuspensionReturns, Integer studentId, StudentRecord studentRecord, StudentRecordJournalDTO studentRecordJournal, List<FundTransfer> fundTransfers, E2Clazz sourceClazz, int targetClazzOriginalAmount, E2Clazz e2Clazz, List<StudentRecordCirculation> circulations, LocalDateTime formModifiedDatetime) {
        log.info("【转班凭单】【凭单行】同步老EMS  {}  {} ", line.getJournalTransferId(), line.getId());
        if ("休学".equals(journal.getExtend3())) {
            suspendSyncs.add(new SuspendSync(
                    record.getStudentId(),
                    line.getOriginClazzId(),
                    line.getOriginClazzOutDate(),
                    journal.getCreatedBy()));
        } else if ("休学归来".equals(journal.getExtend3())) {
            StudentJoinClassInfo origin;
            if (StringUtils.hasText(studentRecord.getAssociatedRecordId())) {
                var or = studentRecordService.getStudentRecordById(studentRecord.getAssociatedRecordId());
                if (or.getClazzId().equals(journal.getTargetClazzId())) {
                    final var sjc = this.studentJoinClassMapper.selectStudentJoinClassId2(
                            studentId, journal.getTargetClazzId(), "SUSPEND"
                    );
                    if (sjc != null) {
                        oldEmsSuspensionReturns.add(sjc);
                        log.info("【休学归来】 {} 查找到休学凭单，休学前班级与转入班班级一致： {}", journal.getId(), sjc);
                        return;
                    }
                }
                var oSourceClazz = e2ClazzMapper.selectByClazzId(or.getClazzId());
                JournalTransferLine cl = CustomBeanUtil.copyProperties(line, new JournalTransferLine());
                cl.setOriginClazzId(or.getClazzId());
                cl.setOriginClazzName(or.getClazzName());
                cl.setOriginClazzInDate(or.getInDate().toLocalDate());
                origin = createOriginSjc(
                        journal,
                        cl, ac, studentRecord.getPrice(),
                        !formModifiedDatetime.isBefore(oSourceClazz.getStartDate().atStartOfDay())
                );
                var ft = new FundTransfer(
                        or.getId(),
                        studentRecordJournal.getTargetStudentRecordId(),
                        line.getMultiPaymentAccountFlag() > 0,
                        StringUtils.hasText(ac.getChannelCode()) ? ac.getChannelCode() : "",
                        "1",
                        journal.getCreatedBy(),
                        line.getOriginPaymentAccountCode(),
                        line.getTargetClazzTransferAmount()
                );
                fundTransfers.add(ft);
                log.info("【休学归来】 {} 查找到休学凭单，转出班参数： {}", journal.getId(), origin);
                log.info("【休学归来】 {} MOCK 凭单行: {}", journal.getId(), cl);
                log.info("【休学归来】 {} 凭费用转移: {}", journal.getId(), ft);
            } else {
                final var sjc = this.studentJoinClassMapper.selectStudentJoinClassId2(
                        studentId, journal.getTargetClazzId(), "SUSPEND"
                );
                if (sjc != null) {
                    oldEmsSuspensionReturns.add(studentId, journal.getTargetClazzId());
                    return;
                } else {
                    origin = createOriginSjc(
                            journal,
                            line, ac, studentRecord.getPrice(),
                            !formModifiedDatetime.isBefore(sourceClazz.getStartDate().atStartOfDay())
                    );
                    var ft = new FundTransfer(
                            line.getOriginStudentRecordId(),
                            studentRecordJournal.getTargetStudentRecordId(),
                            line.getMultiPaymentAccountFlag() > 0,
                            StringUtils.hasText(ac.getChannelCode()) ? ac.getChannelCode() : "",
                            "1",
                            journal.getCreatedBy(),
                            line.getOriginPaymentAccountCode(),
                            line.getTargetClazzTransferAmount()
                    );
                    fundTransfers.add(ft);
                    log.info("【休学归来】 {} 未查找到休学凭单，转出班参数： {}", journal.getId(), origin);
                    log.info("【休学归来】 {} 凭单行: {}", journal.getId(), line);
                    log.info("【休学归来】 {} 凭费用转移: {}", journal.getId(), ft);
                }
            }
            StudentJoinClassInfo target = createTargetSjc(
                    line, ac, journal,
                    record.getDiscounts().stream()
                            .map(StudentRecordChangeDiscountDTO::getDiscountId)
                            .map(Integer::valueOf)
                            .collect(Collectors.toList()),
                    targetClazzOriginalAmount,
                    !line.getTargetClazzInDate().isBefore(e2Clazz.getStartDate())
            );
            log.info("【休学归来】转入班参数： {}", target);
            StudentRecordCirculation c = StudentRecordCirculation
                    .builder()
                    .studentId(record.getStudentId())
                    .originStudentJoinClassInfo(origin)
                    .targetStudentJoinClassInfo(target)
                    .circulationWay(circulationWay(journal.getType(), journal.getExtend3()))
                    .operator(AccountHolder.getRequiredAccountInfo())
                    .build();
            circulations.add(c);
        } else {
            StudentJoinClassInfo origin = createOriginSjc(
                    journal,
                    line, ac, studentRecord.getPrice(),
                    !formModifiedDatetime.isBefore(sourceClazz.getStartDate().atStartOfDay())
            );
            StudentJoinClassInfo target = createTargetSjc(
                    line, ac, journal,
                    record.getDiscounts().stream()
                            .map(StudentRecordChangeDiscountDTO::getDiscountId)
                            .map(Integer::valueOf)
                            .collect(Collectors.toList()),
                    targetClazzOriginalAmount,
                    !line.getTargetClazzInDate().isBefore(e2Clazz.getStartDate())
            );
            StudentRecordCirculation c = StudentRecordCirculation
                    .builder()
                    .studentId(record.getStudentId())
                    .originStudentJoinClassInfo(origin)
                    .targetStudentJoinClassInfo(target)
                    .circulationWay(circulationWay(journal.getType(), journal.getExtend3()))
                    .operator(AccountHolder.getRequiredAccountInfo())
                    .build();
            circulations.add(c);
            var ft = new FundTransfer(
                    line.getOriginStudentRecordId(),
                    studentRecordJournal.getTargetStudentRecordId(),
                    line.getMultiPaymentAccountFlag() > 0,
                    StringUtils.hasText(ac.getChannelCode()) ? ac.getChannelCode() : "",
                    "1",
                    journal.getCreatedBy(),
                    line.getOriginPaymentAccountCode(),
                    line.getTargetClazzTransferAmount()
            );
            fundTransfers.add(ft);
            log.info("【转班】 {} 转出班参数： {}", journal.getId(), origin);
            log.info("【转班】 {} 转入班参数： {}", journal.getId(), target);
            log.info("【转班】 {} 费用转移： {}", journal.getId(), ft);
        }
    }


    @NotNull
    private StudentRecordTransferDTO setResultBeforeTransfer(JournalTransferLine line, JournalTransfer journal, Integer studentId, StudentRecord studentRecord, E2Clazz sourceClazz, int targetClazzOriginalAmount, List<JournalTransferDiscount> journalDiscounts, Map<Integer, EmsDiscountsDO> discountMap, List<JournalTransferLineDiscount> lineDiscounts) {
        final var record = new StudentRecordTransferDTO();
        record.setJournalType(typeTranslateReference(journal.getType()));
        record.setJournalId(journal.getId());
        record.setJournalLineId(line.getId());
        record.setStudentId(studentId);
        record.setTargetClazzId(journal.getTargetClazzId());
        record.setTargetClazzName(journal.getTargetClazzName());
        record.setTargetClazzPrice(journal.getTargetClazzPrice());
        record.setTargetClazzInDate(line.getTargetClazzInDate());
        record.setTargetClazzAmountDue(line.getTargetClazzAmountDue());
        record.setTargetClazzActualAmountDue(line.getTargetClazzActualAmountDue());
        record.setTargetClazzTransferAmount(line.getTargetClazzTransferAmount());
        record.setTargetClazzDiscountAmount(line.getTargetClazzDiscountAmount());
        final var settlementResult = commonService.calculate(
                journal.getType(),
                studentRecord,
                line.getOriginClazzOutDate().atStartOfDay(),
                sourceClazz
        );
        record.setDiscounts(getStudentRecordChangeDiscountDTOS(
                line, journal, studentRecord,
                settlementResult, targetClazzOriginalAmount,
                journalDiscounts, discountMap, lineDiscounts
        ));
        record.setRemark("");
        record.setOriginStudentRecordId(line.getOriginStudentRecordId());
        record.setOriginClazzId(line.getOriginClazzId());
        record.setOriginClazzName(line.getOriginClazzName());
        record.setOriginClazzOutDate(line.getOriginClazzOutDate());
        record.setOriginClazzInDate(line.getOriginClazzInDate());
        record.setOriginClazzServiceDays(line.getOriginClazzServiceDays());
        record.setOriginClazzSettlementDays(line.getOriginClazzSettlementDays());
        record.setOriginClazzActualSettlementDays(line.getOriginClazzActualSettlementDays());
        record.setOriginActualAmountDue(line.getOriginActualAmountDue());
        record.setOriginClazzActualSettlementAmount(line.getOriginClazzActualSettlementAmount());
        record.setOriginClazzSettlementAmount(line.getOriginClazzSettlementAmount());
        return record;
    }

    /**
     * 休学时使用，根据targetStudentRecord找到凭单行
     *
     * @param id
     * @return
     */
    private JournalTransferLine findLineByTargetStudentRecordId(String id) {
        return journalTransferLineMapper.selectOne(new QueryWrapper<JournalTransferLine>().lambda()
                .eq(JournalTransferLine::getDeletedFlag, Boolean.FALSE)
                .eq(JournalTransferLine::getStudentRecordId, id));
    }

    /**
     * 根据凭单类型决定老EMS学籍流转类型
     *
     * @param type    凭单类型
     * @param extend3 凭单类型（附加信息）
     * @return 流转类型
     */
    private CirculationWay circulationWay(JournalTransferType type, String extend3) {
        if ("休学归来".equalsIgnoreCase(extend3)) return CirculationWay.SUSPEND_RETURN;
        return CirculationWay.TRANSITION;
    }

    /**
     * 创建学籍流转信息，转出班
     *
     * @param line
     * @param ac
     * @param price
     * @return
     */
    private StudentJoinClassInfo createOriginSjc(
            JournalTransfer journalTransfer,
            JournalTransferLine line, AdditionalFormPaymentchannel ac, Integer price,
            boolean openClass
    ) {
        if (line.getOriginClazzServiceDays() == 0) {
            return StudentJoinClassInfo
                    .builder()
                    .clazzId(line.getOriginClazzId())
                    .inDate(line.getOriginClazzInDate())
                    .outDate(line.getOriginClazzOutDate())
                    .paymentType(PaymentTypeEum.FIRSTPAYMENT)
                    .price(price)
                    .openClass(openClass)
                    .serviceDays(1)
                    .actualServiceDays(0)
                    .multiChannel(line.getMultiPaymentAccountFlag() > 0)
                    .oriJournalChannelId(
                            StringUtils.hasText(line.getOriginPaymentAccountCode())
                                    ? line.getOriginPaymentAccountCode()
                                    : ac.getChannelCode()
                    )
                    .journalChannelId(ac.getChannelCode())
                    .journalChannelName(ac.getChannelName())
                    .discountIds(null)
                    .build();
        }
        return StudentJoinClassInfo
                .builder()
                .clazzId(line.getOriginClazzId())
                .inDate(line.getOriginClazzInDate())
                .outDate(line.getOriginClazzOutDate())
                .paymentType(PaymentTypeEum.FIRSTPAYMENT)
                .price(price)
                .openClass(openClass)
                .serviceDays(line.getOriginClazzServiceDays())
                .actualServiceDays(line.getOriginClazzActualSettlementDays())
                .multiChannel(line.getMultiPaymentAccountFlag() > 0)
                .oriJournalChannelId(
                        StringUtils.hasText(line.getOriginPaymentAccountCode())
                                ? line.getOriginPaymentAccountCode()
                                : ac.getChannelCode()
                )
                .journalChannelId(ac.getChannelCode())
                .journalChannelName(ac.getChannelName())
                .discountIds(null)
                .build();
    }

    /**
     * 创建学籍流转信息，转入班
     *
     * @param line
     * @param ac
     * @param journal
     * @param d
     * @param targetClazzOriginalAmount
     * @return
     */
    private StudentJoinClassInfo createTargetSjc(
            JournalTransferLine line, AdditionalFormPaymentchannel ac,
            JournalTransfer journal, List<Integer> d, int targetClazzOriginalAmount,
            boolean openClass
    ) {
        return StudentJoinClassInfo
                .builder()
                .clazzId(journal.getTargetClazzId())
                .inDate(line.getTargetClazzInDate())
                .paymentType(PaymentTypeEum.FIRSTPAYMENT)
                // .price(targetClazzOriginalAmount)
                .price(line.getTargetClazzAmountDue() + line.getTargetClazzDiscountAmount())
                .openClass(openClass)
                .multiChannel(line.getMultiPaymentAccountFlag() > 0)
                .oriJournalChannelId(ac.getChannelCode())
                .journalChannelId(ac.getChannelCode())
                .journalChannelName(ac.getChannelName())
                .discountIds(d)
                .build();
    }

    /**
     * 更新数据库内凭单行的学籍信息和状态
     *
     * @param line
     * @param targetStudentRecordId
     * @param journalTransferLineStateEnum
     */
    private void setLineRecordIdAndState(JournalTransferLine line, String targetStudentRecordId, JournalTransferLineStateEnum journalTransferLineStateEnum) {
        journalTransferLineMapper.update(
                null,
                new UpdateWrapper<JournalTransferLine>().lambda()
                        .eq(JournalTransferLine::getId, line.getId())
                        .set(JournalTransferLine::getStudentRecordId, targetStudentRecordId)
                        .set(JournalTransferLine::getState, journalTransferLineStateEnum)
        );
    }

    /**
     * 前端有些字段不填，希望后端补全
     *
     * @param info
     */
    private void updateCheckWebInput(JournalTransferInfoBDto info) {
        if (info.getLineList() == null) {
            info.setLineList(new ArrayList<>());
        }
        if (info.getDiscountList() == null) {
            info.setDiscountList(new ArrayList<>());
        }
        for (JournalTransferInfoBDtoLine journalTransferInfoBDtoLine : info.getLineList()) {
            if (journalTransferInfoBDtoLine.getDiscountList() == null) {
                journalTransferInfoBDtoLine.setDiscountList(new ArrayList<>());
            }
        }
    }

    /**
     * 处理拓展字段
     *
     * @param bo 入参
     */
    private void doReasonExtendField(JournalTransferCreateBo bo) {
        if (StringUtils.hasText(bo.getReasonCode())) {
            var reason = reasonService.all().getByTypeAndCode(
                    bo.getType(),
                    bo.getReasonCode()
            );
            if (reason != null) {
                bo.setExtend1(reason.getCode());
                bo.setExtend2(reason.getName());
            }
        }
        bo.setExtend3(bo.getDirection());
    }

    /**
     * 更新转班主表信息
     *
     * @param info
     */
    private void updateJournalTableInfo(JournalTransferInfoBDto info) {
        // 更新主体数据
        journalTransferMapper.update(
                null,
                new UpdateWrapper<JournalTransfer>()
                        .lambda()
                        .eq(JournalTransfer::getId, info.getId())
                        .set(JournalTransfer::getModifiedBy, AccountHolder.getRequiredAccount())
                        .set(JournalTransfer::getModifiedDatetime, LocalDateTime.now())
                        .set(JournalTransfer::getTargetClazzId, info.getTargetClazzId())
                        .set(JournalTransfer::getTargetClazzName, info.getTargetClazzName())
                        .set(JournalTransfer::getTargetClazzPrice, parsePrice(info.getTargetClazzPriceFormatted()))
                        .set(JournalTransfer::getTargetClazzSubjectId, info.getTargetClazzSubjectId())
                        .set(JournalTransfer::getTargetClazzSubjectName, info.getTargetClazzSubjectName())
                        .set(JournalTransfer::getTargetClazzCampusId, info.getTargetClazzCampusId())
                        .set(JournalTransfer::getTargetClazzCampusName, info.getTargetClazzCampusName())
                        .set(JournalTransfer::getTargetClazzModelId, info.getTargetClazzModelId())
                        .set(JournalTransfer::getTargetClazzModelName, info.getTargetClazzModelName())
                        .set(JournalTransfer::getTransferQuantity, info.getTransferQuantity())
                        .set(JournalTransfer::getState, info.getState())
                        .set(JournalTransfer::getType, info.getType())
                        .set(JournalTransfer::getSpecialFlag, info.getSpecialFlag())
                        .set(JournalTransfer::getRemark, info.getRemark())
                        .set(JournalTransfer::getExtend1, info.getReasonCode())
                        .set(JournalTransfer::getExtend2, info.getReasonDescription())
                        .set(JournalTransfer::getExtend3, info.getDirection())
        );
    }

    /**
     * 移除旧的凭单优惠信息
     *
     * @param journalId 凭单ID
     */
    private void removeOldJournalDiscounts(String journalId) {
        journalTransferDiscountMapper.update(
                null,
                new UpdateWrapper<JournalTransferDiscount>()
                        .lambda()
                        .eq(JournalTransferDiscount::getJournalTransferId, journalId)
                        .set(JournalTransferDiscount::getDeletedBy, AccountHolder.getRequiredAccount())
                        .set(JournalTransferDiscount::getDeletedDatetime, LocalDateTime.now())
                        .set(JournalTransferDiscount::getDeletedFlag, Boolean.TRUE)

        );
    }

    /**
     * 移除旧的凭单行数据
     *
     * @param journalId 凭单ID
     */
    private void removeOldLines(String journalId) {
        journalTransferLineMapper.update(
                null,
                new UpdateWrapper<JournalTransferLine>()
                        .lambda()
                        .eq(JournalTransferLine::getJournalTransferId, journalId)
                        .set(JournalTransferLine::getDeletedBy, AccountHolder.getRequiredAccount())
                        .set(JournalTransferLine::getDeletedDatetime, LocalDateTime.now())
                        .set(JournalTransferLine::getDeletedFlag, Boolean.TRUE));
    }

    /**
     * 移除旧的凭单行优惠数据
     *
     * @param journalId 凭单ID
     */
    private void removeOldLineDiscounts(String journalId) {
        journalTransferLineDiscountMapper.update(
                null,
                new UpdateWrapper<JournalTransferLineDiscount>()
                        .lambda()
                        .eq(JournalTransferLineDiscount::getJournalTransferId, journalId)
                        .set(JournalTransferLineDiscount::getDeletedBy, AccountHolder.getRequiredAccount())
                        .set(JournalTransferLineDiscount::getDeletedDatetime, LocalDateTime.now())
                        .set(JournalTransferLineDiscount::getDeletedFlag, Boolean.TRUE));
    }

    /**
     * 保存优惠信息 校验页面优惠参数、计算行优惠合计（凭单头优惠+行优惠项）
     *
     * @param line
     */
    private void saveLineDiscounts(JournalTransferInfoBDtoLine line) {
        var i = 0;
        for (JournalTransferInfoBDtoLineDiscount journalTransferInfoBDtoLineDiscount : line.getDiscountList()) {
            var discount = new JournalTransferLineDiscount();
            discount.setJournalTransferId(line.getJournalTransferId());
            discount.setJournalTransferLineId(line.getId());
            discount.setStudentRecordId(line.getOriginStudentRecordId());
            discount.setDiscountId(journalTransferInfoBDtoLineDiscount.getDiscountId());
            discount.setDiscountName(journalTransferInfoBDtoLineDiscount.getDiscountName());
            discount.setDiscountType(journalTransferInfoBDtoLineDiscount.getDiscountType());
            discount.setJournalDiscountLineId(journalTransferInfoBDtoLineDiscount.getJournalDiscountLineId());
            discount.setDiscountScopeType(journalTransferInfoBDtoLineDiscount.getDiscountScopeType());
            discount.setDiscountAmount(parsePrice(journalTransferInfoBDtoLineDiscount.getDiscountAmountFormatted()));
            discount.setSort(String.valueOf(i++));
            discount.setDescription("");
            discount.setTenantId(TenantInfo.DEFAULT_TENANT_ID);
            discount.setSubtenantId(TenantInfo.DEFAULT_SUBTENANT_ID);
            discount.setCreatedBy(AccountHolder.getRequiredAccount());
            discount.setCreatedDatetime(LocalDateTime.now());
            discount.setDeletedFlag(Boolean.FALSE);
            journalTransferLineDiscountMapper.insert(discount);
        }
    }

    /**
     * 创建工作流
     *
     * @param type
     * @param id
     * @param specialFlag
     * @param extend3
     * @return
     */
    private String createWorkflow(JournalTransferType type, String id, Boolean specialFlag, String extend3, List<JournalTransferLine> lines) {
        HashMap<String, Object> variables = new HashMap<>();
        // origin_campus
        if (lines.size() == 1) {
            var line = lines.get(0);
            var clazz = e2ClazzMapper.selectByClazzId(line.getOriginClazzId());
            if (clazz != null) {
                variables.put("origin_campus", clazz.getSchoolCode());
            }
        }
        variables.put("handle_matters", typeTranslateName(type));
        switch (type) {
            case TRANSFER, EXCHANGE -> {
                variables.put("class_transfer_type", "开课前转班");
                variables.put("handling_department", "发起人部门");
                variables.put("class_transfer_go_3", "true");
                variables.put("special_approval", String.valueOf(specialFlag));
            }
            case SUSPENSION -> {
                variables.put(
                        "is_suspension_back",
                        "休学归来".equals(extend3)
                );
                variables.put("special_approval", String.valueOf(specialFlag));
            }
            case DELAY -> {
                // todo: check class_transfer_go_3
                variables.put(
                        "is_normal_to_delay",
                        "转入待定班".equals(extend3)
                );
                variables.put("handling_department", "发起人部门");
                variables.put("class_transfer_go_3", "false");
                variables.put("special_approval", String.valueOf(specialFlag));
            }
        }
        return approvalFormService.start(typeTranslateForm(type), id, variables, null);
    }

    /**
     * 获取凭单头
     *
     * @param journalId 凭单ID
     * @return 凭单头
     */
    private JournalTransfer journalTransfer(String journalId) {
        return journalTransferMapper.selectById(journalId);
    }

    /**
     * 获取凭单头的优惠
     *
     * @param journalId 凭单ID
     * @return 凭单头的优惠
     */
    private List<JournalTransferDiscount> journalDiscounts(String journalId) {
        return journalTransferDiscountMapper.selectList(new QueryWrapper<JournalTransferDiscount>().lambda()
                .eq(JournalTransferDiscount::getJournalTransferId, journalId)
                .eq(JournalTransferDiscount::getDeletedFlag, Boolean.FALSE)
                .orderByAsc(JournalTransferDiscount::getSort));
    }

    /**
     * 获取凭单行
     *
     * @param journalId 凭单ID
     * @return 凭单行
     */
    private List<JournalTransferLine> journalLines(String journalId) {
        return journalTransferLineMapper.selectList(new QueryWrapper<JournalTransferLine>().lambda()
                .eq(JournalTransferLine::getJournalTransferId, journalId)
                .eq(JournalTransferLine::getDeletedFlag, Boolean.FALSE)
                .orderByAsc(JournalTransferLine::getCreatedDatetime));
    }


    /**
     * 获取凭单行, 且必须存在, 否则抛出异常, 用于表单提交或者凭单处理结束时
     *
     * @param journalId 凭单ID
     * @return 凭单行
     */
    private List<JournalTransferLine> journalLinesRequired(String journalId) {
        var lines = journalLines(journalId);
        Assert.isTrue(!lines.isEmpty(), "学员列表不能为空");
        return lines;
    }

    /**
     * 获取凭单行的优惠
     *
     * @param journalId 凭单ID
     * @return 凭单行的优惠
     */
    private List<JournalTransferLineDiscount> journalLineDiscounts(String journalId) {
        return journalTransferLineDiscountMapper.selectList(new QueryWrapper<JournalTransferLineDiscount>().lambda()
                .eq(JournalTransferLineDiscount::getJournalTransferId, journalId)
                .eq(JournalTransferLineDiscount::getDeletedFlag, Boolean.FALSE)
                .orderByAsc(JournalTransferLineDiscount::getSort));
    }

    /**
     * 获取凭单行对应的学员id
     *
     * @param srcMap record的map
     * @return 凭单行对应的学员idMap
     */
    private Map<String, Integer> lineIdOriginStudentIdMap(Map<String, StudentRecord> srcMap) {
        var originStudentRecordIdSet = srcMap.values().stream()
                .map(StudentRecord::getId)
                .collect(Collectors.toSet());
        if (originStudentRecordIdSet.isEmpty()) {
            return new HashMap<>();
        }
        var studentRecordIdRelationMap = studentRecordStudentRelationshipMapper.selectList(
                new QueryWrapper<StudentRecordStudentRelationship>()
                        .lambda()
                        .in(StudentRecordStudentRelationship::getStudentRecordId, originStudentRecordIdSet)
        ).stream().collect(Collectors.toMap(StudentRecordStudentRelationship::getStudentRecordId, e -> e));
        Map<String, Integer> result = new HashMap<>();
        for (var e : srcMap.entrySet()) {
            var relation = studentRecordIdRelationMap.get(e.getValue().getId());
            Assert.notNull(relation, "学籍对应学员未找到：" + e.getValue().getId());
            result.put(e.getKey(), relation.getStudentId());
        }
        return result;
    }

    /**
     * 获取凭单行对应的学员记录
     *
     * @param lines 凭单行
     * @return 凭单行对应的学员记录Map
     */
    private Map<String, StudentRecord> lineIdOriginStudentReocrdMap(List<JournalTransferLine> lines) {
        var originStudentRecordIdSet = lines.stream()
                .map(JournalTransferLine::getOriginStudentRecordId)
                .collect(Collectors.toSet());
        if (originStudentRecordIdSet.isEmpty()) {
            return new HashMap<>();
        }
        var studentRecordMap = studentRecordService.getByIds(originStudentRecordIdSet)
                .stream().collect(Collectors.toMap(StudentRecord::getId, e -> e));
        Map<String, StudentRecord> result = new HashMap<>();
        for (JournalTransferLine line : lines) {
            var record = studentRecordMap.get(line.getOriginStudentRecordId());
            Assert.notNull(record, "学籍未找到：" + line.getOriginStudentRecordId());
            result.put(line.getId(), record);
        }
        return result;
    }

    private Map<String, StudentListDto> lineIdStudentMap(Map<String, Integer> lineIdOriginStudentIdMap) {
        if (lineIdOriginStudentIdMap.isEmpty()) {
            return new HashMap<>();
        }
        var query = new StudentDataQueryDto();
        query.setIds(lineIdOriginStudentIdMap.values().stream().toList());
        var studentMap = studentApi.list(query).stream().collect(
                Collectors.toMap(
                        StudentListDto::getId,
                        e -> e
                )
        );
        Map<String, StudentListDto> result = new HashMap<>();
        for (Map.Entry<String, Integer> e : lineIdOriginStudentIdMap.entrySet()) {
            result.put(e.getKey(), studentMap.get(e.getValue()));
        }
        return result;
    }

    private Map<String, StudentRecord> lineIdTargetStudentReocrdMap(List<JournalTransferLineVDto> lines) {
        var targetStudentRecordIdSet = lines.stream()
                .map(JournalTransferLineVDto::getStudentRecordId)
                .filter(StringUtils::hasText)
                .collect(Collectors.toSet());
        if (targetStudentRecordIdSet.isEmpty()) {
            return new HashMap<>();
        }
        var studentRecordMap = studentRecordService.getByIds(targetStudentRecordIdSet)
                .stream().collect(Collectors.toMap(StudentRecord::getId, e -> e));
        Map<String, StudentRecord> result = new HashMap<>();
        for (JournalTransferLineVDto line : lines) {
            var record = studentRecordMap.get(line.getStudentRecordId());
            // Assert.notNull(record, "学籍未找到：" + line.getStudentRecordId());
            result.put(line.getId(), record);
        }
        return result;
    }

    /**
     * 获取凭单行对应的原班级
     *
     * @param lineIdStudentRecordMap 凭单行对应的学员记录
     * @return 凭单行对应的原班级Map
     */
    private Map<String, E2Clazz> lineIdOriginClazzMap(Map<String, StudentRecord> lineIdStudentRecordMap) {
        var clazzIdSet = lineIdStudentRecordMap.values().stream().map(StudentRecord::getClazzId).collect(Collectors.toSet());
        if (clazzIdSet.isEmpty()) {
            return new HashMap<>();
        }
        var clazzMap = e2ClazzMapper.selectList(new QueryWrapper<E2Clazz>().lambda().in(E2Clazz::getClazzId, clazzIdSet))
                .stream().collect(Collectors.toMap(E2Clazz::getClazzId, e -> e));
        Map<String, E2Clazz> result = new HashMap<>();
        for (Map.Entry<String, StudentRecord> entry : lineIdStudentRecordMap.entrySet()) {
            var clazz = clazzMap.get(entry.getValue().getClazzId());
            Assert.notNull(clazz, "班级未找到：" + entry.getValue().getClazzId());
            result.put(entry.getKey(), clazz);
        }
        return result;
    }

    /**
     * 将凭单头和凭单行优惠合并到一起去优惠表里查并返回Map
     *
     * @param journalDiscounts     凭单头优惠
     * @param journalLineDiscounts 凭单行优惠
     * @return Map
     */
    private Map<Integer, EmsDiscountsDO> discountMap(
            Collection<JournalTransferDiscount> journalDiscounts,
            Collection<JournalTransferLineDiscount> journalLineDiscounts
    ) {
        var allDiscountIdSet = Stream.of(
                journalDiscounts.stream().map(JournalTransferDiscount::getDiscountId),
                journalLineDiscounts.stream()
                        .filter(item -> DiscountScopeTypeEnum.NORMAL.name().equalsIgnoreCase(item.getDiscountScopeType()))
                        .map(JournalTransferLineDiscount::getDiscountId)
        ).flatMap(e -> e).collect(Collectors.toSet());
        if (allDiscountIdSet.isEmpty()) {
            return new HashMap<>();
        } else {
            return emsDiscountsMapper.findByIdList(allDiscountIdSet)
                    .stream().collect(Collectors.toMap(EmsDiscountsDO::getId, e -> e));
        }
    }

    /**
     * 获取凭单对应的目标班级
     *
     * @param clazzId
     * @return
     */
    private E2Clazz journalClazz(Integer clazzId) {
        var clazz = e2ClazzMapper.selectOne(new QueryWrapper<E2Clazz>()
                .lambda()
                .eq(E2Clazz::getClazzId, clazzId));
        Assert.notNull(clazz, "班级未找到：" + clazzId);
        return clazz;
    }

    /**
     * 获取凭单对应的目标班级
     *
     * @param vDto
     * @return 目标班级
     */
    private E2Clazz journalClazz(JournalTransferInfoVDto vDto) {
        return journalClazz(vDto.getTargetClazzId());
    }

    /**
     * 获取凭单对应的目标班级
     *
     * @param journalTransfer 凭单
     * @return 目标班级
     */
    private E2Clazz journalClazz(JournalTransfer journalTransfer) {
        return journalClazz(journalTransfer.getTargetClazzId());
    }

    /**
     * 获取凭单对应的目标班级
     *
     * @param journalTransfer 凭单
     * @return 目标班级
     */
    private E2Clazz journalClazz(JournalTransferInfoBDto journalTransfer) {
        return journalClazz(journalTransfer.getTargetClazzId());
    }

    /**
     * 获取凭单对应的目标班级
     *
     * @param journalTransfer 凭单
     * @return 目标班级
     */
    private E2Clazz journalClazz(JournalTransferCreateBo journalTransfer) {
        return journalClazz(journalTransfer.getTargetClazzId());
    }

    /**
     * 获取班级的执行价格（原价）
     *
     * @param clazz 班级
     * @return 执行价格（原价）
     */
    private int clazzOriginAmount(E2Clazz clazz) {
        return tuitionFeeService.findTuition(clazz.getClazzId(), null).multiply(new BigDecimal(100)).intValue();
    }

    @NotNull
    private List<StudentRecordChangeDiscountDTO> getStudentRecordChangeDiscountDTOS(JournalTransferLine line, JournalTransfer journal, StudentRecord studentRecord, SettlementResult settlementResult, int targetClazzOriginalAmount, List<JournalTransferDiscount> journalDiscounts, Map<Integer, EmsDiscountsDO> discountMap, List<JournalTransferLineDiscount> lineDiscounts) {
        var shouldPayStart = switch (journal.getType()) {
            case EXCHANGE, SUSPENSION, DELAY ->
                    studentRecord.getActualAmountDue() - settlementResult.getSettlementAmount();
            default -> targetClazzOriginalAmount;
        };
        var shouldPayStartBD = new BigDecimal(shouldPayStart).divide(new BigDecimal(100));
        var discountDtos = Stream.of(
                journalDiscounts.stream().map(e -> {
                    var discount = discountMap.get(e.getDiscountId());
                    var dis = new BigDecimal(discountsService.transferDiscountDTO(discount, shouldPayStartBD).getShowDiscountMoney()).multiply(new BigDecimal(100)).intValue();
                    return new StudentRecordChangeDiscountDTO(
                            String.valueOf(e.getDiscountId()),
                            dis,
                            Byte.valueOf(e.getSort())
                    );
                }),
                lineDiscounts.stream()
                        .filter(e -> e.getJournalTransferLineId().equals(line.getId()) && DiscountScopeTypeEnum.NORMAL.name().equalsIgnoreCase(e.getDiscountScopeType()))
                        .map(e -> {
                            var discount = discountMap.get(e.getDiscountId());
                            var dis = new BigDecimal(discountsService.transferDiscountDTO(discount, shouldPayStartBD).getShowDiscountMoney()).multiply(new BigDecimal(100)).intValue();
                            return new StudentRecordChangeDiscountDTO(
                                    String.valueOf(e.getDiscountId()),
                                    dis,
                                    Byte.valueOf(e.getSort())
                            );
                        }),
                lineDiscounts.stream()
                        .filter(e -> e.getJournalTransferLineId().equals(line.getId()) && DiscountScopeTypeEnum.PERSONAL.name().equalsIgnoreCase(e.getDiscountScopeType()))
                        .map(e -> {
                            JournalDiscountApiResDTO resDTO = journalDiscountApi.queryByLineId(JournalDiscountApiReqDTO.buildForQueryByLineId(e.getJournalDiscountLineId()));
                            cn.hutool.core.lang.Assert.notNull(resDTO, BizExceptionProducer.produce(NemsErrorBodyEnum.DATA_EXIST, "个人优惠ID：" + e.getJournalDiscountLineId()));
                            var dis = NumberUtil.convertToCent(resDTO.getDiscountValue());
                            // 如果优惠类型为 “折扣” ， 优惠金额 =  执行价格 *  折扣
                            if (DiscountTypeEnum.DISCOUNT.name().equalsIgnoreCase(resDTO.getDiscountType())) {
                                dis = NumberUtil.convertToCent(shouldPayStartBD.multiply(resDTO.getDiscountValue()));
                            }
                            return new StudentRecordChangeDiscountDTO(
                                    String.valueOf(e.getDiscountId()),
                                    dis,
                                    Byte.valueOf(e.getSort())
                            );
                        })
        ).flatMap(e -> e).toList();
        var allDiscountAmount = discountDtos.stream().map(StudentRecordChangeDiscountDTO::getDiscountAmount).reduce(Integer::sum).orElse(0);
        if (allDiscountAmount > shouldPayStart) {
            var sum = 0;
            for (StudentRecordChangeDiscountDTO discountDto : discountDtos) {
                if (discountDto.getDiscountAmount() + sum >= shouldPayStart) {
                    discountDto.setDiscountAmount(shouldPayStart - sum);
                    sum = shouldPayStart;
                } else {
                    sum += discountDto.getDiscountAmount();
                }
            }
        }
        return discountDtos;
    }

    /**
     * 根据学籍id寻找支付渠道, 支持多个，并组成map返回
     *
     * @param recordIdSet 学籍id集合
     * @return 学籍id和支付渠道的map
     */
    private Map<String, FlowDataObj> recordChannelMap(Collection<String> recordIdSet) {
        var fundFlows = fundFlowService.listStudentRecordFundFlows(recordIdSet);
        var fundFlowsGroupedByRecord = fundFlows.stream().collect(Collectors.groupingBy(StudentRecordFundFlow::getStudentRecordId));
        Map<String, FlowDataObj> result = new HashMap<>();
        for (String recordId : recordIdSet) {
            Integer multiPaymentAccountFlag = null;
            String paymentAccountCode = null;
            String paymentAccountName = null;
            var sFundFlows = fundFlowsGroupedByRecord.get(recordId);
            if (sFundFlows != null) {
                for (StudentRecordFundFlow f : sFundFlows) {
                    if (paymentAccountCode == null) {
                        multiPaymentAccountFlag = 0;
                        paymentAccountCode = f.getPaymentAccountCode();
                        paymentAccountName = f.getPaymentAccountName();
                    } else if (paymentAccountCode.equals(f.getPaymentAccountCode())) {
                        continue;
                    } else {
                        multiPaymentAccountFlag = 1;
                        paymentAccountCode = null;
                        paymentAccountName = null;
                        break;
                    }
                }
            }
            result.put(recordId, new FlowDataObj(multiPaymentAccountFlag, paymentAccountCode, paymentAccountName));
        }
        return result;
    }

    /**
     * 对单行的表单原学籍支付信息进行赋值
     *
     * @param line 单行表单
     */
    private void updateSingleLinePaidInfo(JournalTransferLine line) {
        if (Objects.isNull(line)) {
            return;
        }
        var recordChannelMap = recordChannelMap(List.of(line.getOriginStudentRecordId()));
        var rc = recordChannelMap.get(line.getOriginStudentRecordId());
        if (Objects.isNull(rc)) {
            return;
        }
        line.setMultiPaymentAccountFlag(rc.multiPaymentAccountFlag);
        line.setOriginPaymentAccountCode(rc.paymentAccountCode);
        line.setOriginPaymentAccountName(rc.paymentAccountName);
    }

    /**
     * 保存凭单行和订单的关系
     * <p>
     *
     * @param line    凭单行
     * @param journal 凭单
     * @param dto     学员记录转换结果
     */
    private void saveOrderRelationship(JournalTransferLine line, JournalTransfer journal, StudentRecordChangeResultDTO dto) {
        var ship = new JournalTransferOrderRelationship();
        ship.setJournalTransferId(journal.getId());
        ship.setJournalTransferLineId(line.getId());
        ship.setOrderId(dto.getOrderId());
        ship.setStudentRecordId(dto.getTargetStudentRecordId());
        ship.setTenantId(TenantInfo.DEFAULT_TENANT_ID);
        ship.setSubtenantId(TenantInfo.DEFAULT_SUBTENANT_ID);
        journalTransferOrderRelationshipMapper.insert(ship);
    }

    /**
     * 凭单结束以后，凭单行应该呈现的状态
     *
     * @param journalTransferType 凭单类型
     * @return 状态
     */
    private JournalTransferLineStateEnum lineFinishState(JournalTransferType journalTransferType) {
        return switch (journalTransferType) {
            case TRANSFER -> JournalTransferLineStateEnum.ORDER_CREATED;
            case EXCHANGE, SUSPENSION, DELAY -> JournalTransferLineStateEnum.FINISHED;
        };
    }

    /**
     * 将凭单置为已完成
     *
     * @param journalId 凭单ID
     */
    private void updateJournalToFinish(String journalId) {
        updateJournalToState(journalId, ApprovalFormStateEnum.FINISHED);
    }

    /**
     * 校验财务附加表单
     * <p>
     * 如果用户填了并且完整返回非null, 否则返回null
     * </p>
     * <p>
     * 有优化空间：提示具体哪些值没有填写
     * </p>
     *
     * @param formID    表单ID
     * @param journalId 凭单ID
     * @return 非null表示表单完整，null表示表单不完整
     */
    private AdditionalFormPaymentchannel checkAddForm(String formID, String journalId) {
        Map<String, Object> map = approvalAdditionalFormService.formAdditionalFroms(formID);
        AdditionalFormPaymentchannel c;
        try {
            c = new ObjectMapper()
                    .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
                    .configure(JsonGenerator.Feature.IGNORE_UNKNOWN, true)
                    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                    .convertValue(map, AdditionalFormPaymentchannel.class);
        } catch (Exception e) {
            log.error("【审批转班凭单】附加表单转换失败 (final)", e);
            return null;
        }
        if (c.getBizDate() == null) return null;
        if (!StringUtils.hasText(c.getCampusId())) return null;
        if (!StringUtils.hasText(c.getChannelCode())) return null;
        return c;
    }

    /**
     * 表单校验不通过时将审批流程单设置为未通过，解锁学生
     *
     * @param formID    表单ID
     * @param journalId 凭单ID
     */
    private void doCheckFail(String formID, String journalId, ApprovalErrorBodyEnum errorBodyEnum) {
        unlockLineRecords(journalId);
        updateJournalToState(journalId, JournalStateEnum.REJECTED);
        approvalFormService.fail(formID, errorBodyEnum.getMessage());
    }

    /**
     * 更新凭单的状态
     *
     * @param journalId
     * @param state
     */
    private void updateJournalToState(String journalId, ApprovalFormStateEnum state) {
        JournalTransfer updateEntity = new JournalTransfer();
        updateEntity.setId(journalId);
        updateEntity.setState(state.name());
        updateById(updateEntity);
    }

    /**
     * 更新凭单的状态
     *
     * @param journalId
     * @param state
     */
    private void updateJournalToState(String journalId, JournalStateEnum state) {
        JournalTransfer updateEntity = new JournalTransfer();
        updateEntity.setId(journalId);
        updateEntity.setState(state.name());
        updateById(updateEntity);
    }

    /**
     * 根据凭单ID解锁学籍Id
     *
     * @param journalId
     */
    private void unlockLineRecords(String journalId) {
        unlockLineRecords(journalTransferMapper.selectById(journalId));
    }

    /**
     * 根据凭单锁定学籍ID
     *
     * @param recordIdSet
     * @param journalType
     * @param journalId
     */
    public void lockLineRecords(Set<String> recordIdSet, JournalTransferType journalType, String journalId) {
        this.studentRecordService.locks(
                recordIdSet,
                typeTranslateReference(journalType),
                journalId
        );
    }

    /**
     * 根据凭单解锁学籍ID
     *
     * @param journal
     */
    private void unlockLineRecords(JournalTransfer journal) {
        final var lines = journalLines(journal.getId());
        unlockLineRecords(
                journal.getId(),
                journal.getType(),
                lines
                        .stream()
                        .map(JournalTransferLine::getOriginStudentRecordId)
                        .collect(Collectors.toSet())
        );
    }

    /**
     * 调用解锁学籍的接口
     *
     * @param journalId
     * @param journalType
     * @param recordIdSet
     */
    private void unlockLineRecords(
            String journalId,
            JournalTransferType journalType,
            Set<String> recordIdSet
    ) {
        this.studentRecordService.unlocks(
                recordIdSet,
                typeTranslateReference(journalType),
                journalId
        );
    }

    // “转班”、“转移” 使用原ID生成逻辑，“休学”、“转待定”使用新的ID生成逻辑
    private String journalId(JournalTransferType type) {
        return switch (type) {
            case TRANSFER, EXCHANGE -> bizNumberService.next(BizNumberTypeEnum.TRANSFER);
            case SUSPENSION -> bizNumberService.next(BizNumberTypeEnum.SUSPENSION);
            case DELAY -> bizNumberService.next(BizNumberTypeEnum.DELAY);
        };
    }

    /**
     * 将类型进行转义成引用类型
     *
     * @param journalTransferType 转班类型
     * @return 引用类型
     */
    private ReferenceTypeEnum typeTranslateReference(JournalTransferType journalTransferType) {
        return switch (journalTransferType) {
            case TRANSFER -> ReferenceTypeEnum.TRANSFER;
            case EXCHANGE -> ReferenceTypeEnum.EXCHANGE;
            case SUSPENSION -> ReferenceTypeEnum.SUSPENSION;
            case DELAY -> ReferenceTypeEnum.DELAY;
        };
    }

    /**
     * 单行表单额外的处理
     *
     * @param bo 入参
     */
    private void processSingleLine(JournalTransferCreateBo bo) {
        if (bo.getSingleLine() != null) {
            var line = bo.getSingleLine();
            line.setJournalTransferId(bo.getId());
            line.setState(bo.getState());
            updateSingleLinePaidInfo(line);
            journalTransferLineMapper.insert(line);
            var record = studentRecordService.getById(line.getOriginStudentRecordId());
            Assert.notNull(record, "学员学籍数据未找到");
            Assert.isTrue("VALID".equals(record.getState()), "学员学籍状态不是有效状态");
            Assert.isTrue(
                    !bo.getTargetClazzId().equals(record.getClazzId()),
                    "转入班不能和转出班相同"
            );
            Assert.isTrue(!record.getLockFlag(), "学籍已经被锁定");
            Assert.isTrue(record.getSettlementDate() == null, "学员已经出班");
            var studentId = studentRecordStudentRelationshipMapper.selectStudentId(record.getId());
            Assert.notNull(studentId, "学员未找到");
            var count = studentRecordStudentRelationshipMapper.countValid(studentId, bo.getTargetClazzId());
            Assert.isTrue(count == 0, "学员已经在目标班级, 且学籍有效");
            if ("PENDING".equals(bo.getState())) {
                lockLineRecords(Set.of(line.getOriginStudentRecordId()), bo.getType(), bo.getId());
                this.createWorkflow(bo.getType(), bo.getId(), bo.getSpecialFlag(), bo.getExtend3(), List.of(line));
            }
        }
    }

    /**
     * 将类型进行转义成表单用的凭单类型
     *
     * @param journalTransferType 转班类型
     * @return 名称
     */
    private JournalType typeTranslateForm(JournalTransferType journalTransferType) {
        return switch (journalTransferType) {
            case TRANSFER -> JournalType.TRANSFER;
            case EXCHANGE -> JournalType.EXCHANGE;
            case SUSPENSION -> JournalType.SUSPENSION;
            case DELAY -> JournalType.DELAY;
        };
    }

    /**
     * 将类型进行转义成中文名称
     *
     * @param journalTransferType 转班类型
     * @return 名称
     */
    private String typeTranslateName(JournalTransferType journalTransferType) {
        return switch (journalTransferType) {
            case TRANSFER -> "转移";
            case EXCHANGE -> "转班";
            case SUSPENSION -> "休学";
            case DELAY -> "转待定班";
        };
    }

    private ApprovalConfig getApprovalConfig(
            List<JournalTransferType> types
    ) {
        JournalType jt;
        if (types == null || types.isEmpty()) {
            jt = JournalType.TRANSFER;
        } else if (types.contains(JournalTransferType.SUSPENSION)) {
            jt = JournalType.SUSPENSION;
        } else if (types.contains(JournalTransferType.DELAY)) {
            jt = JournalType.DELAY;
        } else {
            jt = JournalType.TRANSFER;
        }
        ApprovalConfig config = approvalConfigService.getByJournalCode(jt.name());
        cn.hutool.core.lang.Assert.notNull(config, BizExceptionProducer.produce(ApprovalErrorBodyEnum.MUST_CONFIG_APPROVAL_CONFIG, jt.getCnName()));
        return config;
    }

    @AllArgsConstructor
    private static final class FlowDataObj {
        private Integer multiPaymentAccountFlag;
        private String paymentAccountCode;
        private String paymentAccountName;
    }

    @AllArgsConstructor
    private static class DoFinishResult {
        List<StudentRecordCirculation> circulations;
        List<PaymentFlowDTO> flowDTOs;
        List<SuspendSync> suspendSyncs;
        List<FundTransfer> fundTransfers;
        List<Integer> oldEmsSuspensionReturns;
    }

    @AllArgsConstructor
    private static class SuspendSync {
        Integer studentId;
        Integer clazzId;
        LocalDate suspendDate;
        String operatorEmail;
    }

    @Data
    @AllArgsConstructor
    private static class FundTransfer {
        private String originStudentRecordId;
        private String targetStudentRecordId;
        private Boolean multiPaymentAccountFlag;
        private String channel;
        private String type;
        private String createdBy;
        private String originChannelCode;
        private Integer transferAmount;
    }

    /**
     * 对lineDiscounts进行分组
     *
     * @param lineDiscounts
     * @return
     */
    @NotNull
    private static Map<String, List<JournalTransferLineDiscountVDto>> groupLineDiscounts(List<JournalTransferLineDiscount> lineDiscounts) {
        return lineDiscounts.stream().map(e -> {
            var vo = CustomBeanUtil.copyProperties(e, new JournalTransferLineDiscountVDto());
            vo.setDiscountAmountFormatted(formatPrice(e.getDiscountAmount()));
            return vo;
        }).collect(Collectors.groupingBy(JournalTransferLineDiscountVDto::getJournalTransferLineId));
    }

    @NotNull
    private Map<Integer, EmsDiscountsDO> emsDiscoutMap(Collection<Integer> discountId) {
        if (CollectionUtils.isEmpty(discountId)) {
            return new HashMap<>();
        }
        return emsDiscountsMapper.findByIdList(discountId)
                .stream().collect(Collectors.toMap(EmsDiscountsDO::getId, e -> e));
    }

    private int setTargetClazzInfoAndReturnOriginAmount(JournalTransferInfoBDto info) {
        var e2Clazz = journalClazz(info);
        info.setTargetClazzName(e2Clazz.getClazzName());
        Assert.isTrue(e2Clazz.getSchoolCode().equals(info.getTargetClazzCampusId()), "校区不匹配");
        info.setTargetClazzCampusName(e2Clazz.getSchoolName());
        var targetClazzOriginalAmount = tuitionFeeService.findTuition(e2Clazz.getClazzId(), null).multiply(new BigDecimal(100)).intValue();
        info.setTargetClazzPriceFormatted(formatPrice(targetClazzOriginalAmount));
        info.setTargetClazzModelName(TeachingModelEnum.valueOf(info.getTargetClazzModelId()).getName());
        info.setTargetClazzSubjectName(subjectApi.findById(info.getTargetClazzSubjectId()).getName());
        return targetClazzOriginalAmount;
    }

    private void checkRecords(JournalTransferInfoBDto info) {
        Assert.isTrue(info.getLineList().stream().map(JournalTransferInfoBDtoLine::getOriginStudentRecordId)
                .peek(e -> Assert.notNull(e, "学籍ID不能为空"))
                .collect(Collectors.toSet()).size() == info.getLineList().size(), "学籍ID不能重复");
    }

    private Map<Integer, EmsDiscountsDO> mergeDiscountMap(List<JournalTransferDiscount> discounts, List<JournalTransferLineDiscount> lineDiscounts) {
        var discountIdSet = Stream.of(
                discounts.stream().map(JournalTransferDiscount::getDiscountId),
                lineDiscounts.stream().filter(
                        i -> DiscountScopeTypeEnum.NORMAL.name().equalsIgnoreCase(i.getDiscountScopeType())
                ).map(JournalTransferLineDiscount::getDiscountId)
        ).flatMap(e -> e).collect(Collectors.toSet());
        return emsDiscoutMap(new ArrayList<>(discountIdSet));
    }

}
